转载

[译] 字段访问、属性和描述符 2

注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python

__getattribute__()方法

__getattribute__() 方法是一个更底层的属性处理。它的默认实现试图把一个字段作为一个已经存在于内部 __dict__ (或 __slots__ )的字段来定位值。如果没有找到该字段,它会调用 __getattr__() 。如果值被定位为描述符(参见下面《创建描述符》部分),则处理描述符;否则只是简单的返回值。

通过重写此方法,我们可以完成以下任何一个任务:

  • 我们可以有效地防止对字段的访问。这种方法通过抛出异常来代替返回一个值,可以使一个字段比我们仅仅使用下划线(_)将一个命名标记为私有更私密。

  • 我们可以发明新的字段,类似于 __getattr__() 如何发明新的字段。然而,在这种情况下,我们可以通过默认版本的 __getattribute__() 来绕过默认查找。

  • 我们可以让字段执行唯一且不同的任务。这会使得程序非常难以理解和维护。这是一个糟糕的想法。

  • 我们可以改变描述符的行为。虽然技术上可能,但改变一个描述符的行为是一个可怕的想法。

在我们实现 __getattribute__() 方法时,重要的是要注意在方法体中不能有任何的内部字段访问。如果我们试图通过 self.name 获取值,将导致无限递归。

__getattribute__() 方法不能提供任何简单的 self.name 字段访问,这将导致无限递归。

为了在 __getattribute__() 方法中获取字段值,我们必须显式地访问 object 定义的基础方法,如以下所示声明:

object.__getattribute__(self, name)

例如,我们可以使用 __getattribute__() 修改我们的不可变类以及防止访问内部 __dict__ 字段。下面这个类,隐藏了所有以下划线(_)开头的命名:

class BlackJackCard3: """Abstract Superclass""" def __init__(self, rank, suit, hard, soft):  super().__setattr__('rank', rank)  super().__setattr__('suit', suit)  super().__setattr__('hard', hard)  super().__setattr__('soft', soft) def __setattr__(self, name, value):  if name in self.__dict__:   raise AttributeError("Cannot set {name}".format(name=name))  raise AttributeError("'{__class__.__name__}' has no attribute  '{name}'".format(__class__= self.__class__, name= name)) def __getattribute__(self, name):  if name.startswith('_'):   raise AttributeError  return object.__getattribute__(self, name) 

我们已经覆写了 __getattribute__() 的私有名称以及Python内部名称来抛出一个字段错误。这前面的示例有一个微小的优势:我们不再允许调整对象。我们将会看到该类的实例交互的示例。

下面示例是该类对象的变形:

>>> c = BlackJackCard3('A', '♠', 1, 11) >>> c.rank = 12 Traceback (most recent call last):   File "<stdin>", line 1, in <module>   File "<stdin>", line 9, in __setattr__   File "<stdin>", line 13, in __getattribute__ AttributeError >>> c.__dict__['rank']= 12 Traceback (most recent call last):   File "<stdin>", line 1, in <module>   File "<stdin>", line 13, in __getattribute__ AttributeError

通常建议,搞混 __getattribute__() 不是一个好主意。默认方法相当复杂,而且几乎所有我们需要的都是作为属性利用或作为 __getattr__() 的改变。

创建描述符

描述符是调和字段访问的一个类。描述符类可用来获取、设置或删除字段值。描述符对象是在类定义的时候构建在一个类中的。

描述符设计模式有两个部分:一个 所有者类字段描述符 本身。所有者类给它的字段使用一个或多个描述符。描述符类定义了获取、设置和删除方法的组合。描述符类的一个实例将会是所有者类的一个字段。

属性是基于所有者类的方法函数。描述符不像属性,是一个类的实例,与所有者类不同。因此,描述符通常是可重用的通用字段。所有者类可以有多个不同描述符类的实例来类管理具有相似行为的字段。

不像其他字段,描述符在类级别上创建。它们不是在 __init()__ 初始化时创建。然而描述符的值可以在初始化期间设置,描述符通常是作为类的一部分,在任何方法函数之外来构建的。

当所有者类被定义时,每个描述符对象都是被绑定到一个不同的类级别字段的描述符类实例。

被确认为一个描述符,一个类必须实现以下三个方法的任意组合。

  • Descriptor.__get__(self, instance, owner) -> object :在这个方法中, instance 参数是即将被访问的对象的 self 变量。 owner 参数是所有者类的对象。如果这个描述符在类的上下文中被调用, instance 参数将得到一个 None 值。这必须返回描述符的值。

  • Descriptor.__set__(self, instance, value) :在这个方法中, instance 参数是即将被访问的对象的 self 变量。 value 参数是描述符需要设置的新值。

  • Descriptor.__delete__(self, instance) 在这个方法中, instance 参数是即将被访问的对象的 self 变量。该描述符的方法必须删除这个字段的值。

有时,一个描述符类还将需要一个 __init__() 方法函数来初始化描述符的内部状态。

有两种基于已定义方法的描述符,如下所示:

  • 非数据描述符:这种描述符定义 __set__()__delete__() 或两者皆有。它不能定义 __get__() 。非数据描述符对象往往会被用作表达式的一部分。它可能是一个可调用对象,或者它可能有自己的字段或方法。一个不可变的非数据描述符必须实现 __set__() ,但可能只是抛出 AttributeError 。这些描述符设计时很简单,因为接口更灵活。

  • 数据描述符:这种描述符至少定义 __get__() 。通常,它定义 __get__()__set__() 来创建一个可变对象。鉴于描述符将在很大程度上是不可见的,则不能更进一步的再定义字段或方法。字段的引用有一个数据描述符的数据被委托给描述符的 __get__()__set__()__delete__() 方法。这些是很难设计的,所以我们稍后来再看。

描述符有各种各样的用例。在内部,Python使用描述符有以下几个原因:

  • 在隐藏的内部,类的方法是作为描述符来实现。这些非数据描述符应用方法函数到对象以及不同的参数值。

  • property() 函数通过给一个字段创建数据描述符来实现。

  • 一个类方法或静态方法被实现为一个描述符;这被应用到类中来代替类的实例。

当我们在第11章《通过SQLite存储和检索对象》看到对象-关系映射的时候,我们将看到许多ORM类定义大量使用描述符将Python类映射到SQL表和列。

当我们考虑一个描述符的目的,我们还必须为数据作为描述符可以正常工作来考察三种常见用例,如下所示:

  • 描述符对象有数据或获取到了数据。在这种情况下,描述符对象的 self 变量是有意义的且描述符是有状态的。数据描述符的 __get__() 方法返回这个内部数据。非数据描述符,描述符有其他方法或字段来访问这些数据。

  • 包含数据的 所有者实例 。在这种情况下,描述符对象必须使用 instance 参数来引用值到所有者对象中。数据描述符的 __get__() 方法从实例获取数据。非数据描述符有其他方法访问实例数据。

  • 包含相关数据的 所有者类 。在这种情况下,描述符对象必须使用 owner 参数。这是常用的当描述符实现了应用于整个类的静态方法或类方法。

我们将仔细看下第一种情况。我们看看创建带有 __get__()__set__() 方法的数据描述符。我们也会看看创建没有 __get__() 方法的非数据描述符。

第二种情况(所有者实例中的数据)展示了 @property 装饰器都做了些什么。可能的优势是描述符有一个传统的属性将计算从拥有者类移到描述符类中。这倾向于分片类设计且可能不是最好的方法。如果计算是真正史诗般的复杂,策略模式可能会更好。

第三种情况展示 @staticmethod@classmethod 装饰器是如何实现的。我们不需要重新发明轮子。

1、使用非数据描述符

我们经常会有一些紧密绑定了字段值的小对象。对于这个示例,我们将看看数值被绑定到单位的举措。

下面是一个简单的非数据描述符类,它缺少一个 __get__() 方法:

class UnitValue_1: """Measure and Unit combined.""" def __init__(self, unit):  self.value = None  self.unit = unit  self.default_format = "5.2f" def __set__(self, instance, value):  self.value = value def __str__(self):  return "{value:{spec}} {unit}"  .format(spec=self.default_format, **self.__dict__) def __format__(self, spec="5.2f"):  #print( "formatting", spec )  if spec == "":   spec = self.default_format  return "{value:{spec}} {unit}".format(spec=spec, **self.__dict__) 

这个类定义了一对简单的值,一个可变的(值),另一个是有效的不可变对象(单位)。

当这个描述符被访问时,描述符对象本身是可用的,且描述符的其他方法或字段可以被使用。我们可以使用这个描述符来创建类去管理尺寸和其他与物理单位有关的数值。

下面是一个类,做速度-时间-距离的及早计算:

class RTD_1: rate = UnitValue_1("kt") time = UnitValue_1("hr") distance = UnitValue_1("nm") def __init__(self, rate=None, time=None, distance=None):  if rate is None:   self.time = time   self.distance = distance   self.rate = distance / time  if time is None:   self.rate = rate   self.distance = distance   self.time = distance / rate  if distance is None:   self.rate = rate   self.time = time   self.distance = rate * time def __str__(self):  return "rate: {0.rate} time: {0.time} distance:{0.distance}".format(self) 

一旦对象被创建且字段被加载,丢失的值就已经被计算。一旦计算,描述符可以检查获取值或单位的名称。此外,描述符对 str() 有一个方便的响应和请求格式。

下面是描述符和 RTD_1 类之间的交互:

>>> m1 = RTD_1(rate=5.8, distance=12) >>> str(m1) 'rate:  5.80 kt time:  2.07 hr distance: 12.00 nm' >>> print("Time:", m1.time.value, m1.time.unit) Time: 2.0689655172413794 hr

我们创建了一个带有 ratedistance 参数的 RTD_1 实例。这些都是用来计算 ratedistance 描述符的 __set__() 方法。

当我们请求 str(m1) ,这会计算 RTD_1 的所有 str() 方法,转而使用 ratetimedistance 描述符的 __format__() 方法。这为我们提供了数字和单位。

鉴于非数据描述符没有 __get__() 且不返回其内部值,我们可以访问描述符的单个元素。

2、使用数据描述符

数据描述符设计要复杂一些,因为它对接口有限制。它必须有一个 __get__() 方法,且只能有 __set__()__delete__() 。这是所有的接口:这些方法从一到三,没有其他方法。引入一个额外的方法意味着Python不会把该类当作一个正确的数据描述符。

我们会使用描述符设计一个简单的单位转换模式,可以在 __get__()__set__() 方法做适当的转换。

下面是一个单位描述符的超类,它在其他单位和标准单位之间做转换:

class Unit:     conversion = 1.0     def __get__(self, instance, owner):         return instance.kph * self.conversion      def __set__(self, instance, value):         instance.kph = value / self.conversion

该类用简单的乘法和除法将标准单位转换为其他非标准单位,反之亦然。

通过这个超类,我们可以从一个标准单位定义一些转换。在前面的示例,标准单位是千米时(公里/小时)。

以下是这两个转换描述符

class Knots(Unit):     conversion = 0.5399568  class MPH(Unit):     conversion = 0.62137119

继承方法非常有用。唯一改变的是转换因子。这些类可用于处理涉及单位转换的值。我们可以处理英里每小时或可交换的节点。下面是一个标准单位的单位描述符,公里每小时:

class KPH(Unit):      def __get__(self, instance, owner):         return instance._kph      def __set__(self, instance, value):         instance._kph = value

这个类代表一个标准,所以不做任何转换。它使用一个私有变量实例保存速度千米每小时的标准值。避免任何算术转换是一个简单的技术优化。避免任何一个公共字段的引用是至关重要的,来规避无限递归。

下面这个类,它对于一个给定的尺寸提供了一组转换:

class Measurement: kph = KPH() knots = Knots() mph = MPH() def __init__(self, kph=None, mph=None, knots=None):  if kph:   self.kph = kph  elif mph:   self.mph = mph  elif knots:   self.knots = knots  else:   raise TypeError def __str__(self):  return "rate: {0.kph} kph = {0.mph} mph = {0.knots} knots".format(self) 

对于不同的单位每个类级别的字段都是描述符。各种描述符的获取和设置方法会做适当的转换。我们可以使用这个类在各种单位之间进行速度转换。

以下是与 Measurement 类交互的一个例子:

>>> m2 = Measurement(knots=5.9) >>> str(m2) 'rate: 10.92680006993152 kph = 6.789598762345432 mph = 5.9 knots' >>> m2.kph 10.92680006993152 >>> m2.mph 6.789598762345432

我们通过设置不同的描述符创建了一个 Measurement 类的对象。在第一个示例中,我们设置了节点描述符。

当我们显示的值是一个大字符串,则每个描述符的 __get__() 都将被使用。这些方法从所有者对象获取内部 kph 字段值,应用一个转换因子,且返回一个结果值。

kph 字段还使用了一个描述符。这个描述符不做任何转换;然而,它只是返回了缓存在所有者对象的私有值。 KPHKnots 描述符要求所有者类实现一个 kph 字段。

总结,设计要素和权衡

在这一章,我们研究了几种使用一个对象字段的方式。我们可以使用内置 object 类的特性以及获取和设置字段值。我们可以定义属性来修改字段的行为。

如果我们想要更复杂,我们可以调整底层 __getattr__()__setattr__()__delattr__()__getattribute__() 特殊方法实现。这些让我们可以更精细的控制字段的行为。当我们接触到这些方法我们走的很顺利,因为我们可以对Python的行为进行基本(和令人困惑的)改变。

在内部,Python使用描述符来实现特性,例如方法函数、静态方法函数和属性。描述符许多很酷的用例已经是语言的最好的特性。

来自其他语言(特别是Java和c++)的程序员通常有试图让所有字段私有以及编写大量的 gettersetter 函数的冲动。

在Python中,将所有字段作当作公有的是相当简单的。这意味着如下:

  • 它们应该有良好的文档记录。

  • 它们应该正确的反映对象的状态,它们不应该是暂时的或临时的值。

  • 一个字段有令人困惑的(或易变的)字段值是非常罕见的,一个以下划线(_)开头的命名作为“不是已定义接口中的一部分”不是真的私有。

把私有字段看做麻烦事是很重要的。在语言中封装并没有因为缺乏复杂的私有机制而受损;而会因为糟糕的设计而受损。

1、属性对字段

在大多数情况下,字段可以设置在类之外且没有不良后果。我们的 Hand 类的示例展示了这一点。对于许多版本的类,我们可以简单地追加到 hand.cards ,以及完美的工作通过属性延迟计算 total

在改变字段这种情况下,会导致相应的其他字段的变化,这需要一些更复杂的类设计:

  • 一个方法函数可以阐明状态变化。当需要多个参数值时这将是必要的。

  • 一个属性 setter 可能比一个方法函数更清晰。当需要单个值时这将是一个明智的选择。

  • 我们还可以使用原地操作符。我们将到第七章《创造数字》看到这些。

没有严格的规则。在这种情况下,我们需要设置一个参数值,方法函数和属性之间的区别完全是API语法和如何传达意图的区别。

为了计算值,属性允许延迟计算,而一个字段需要及早计算。这是性能的问题。延迟计算对及早计算的优势是基于对用例的预期。

2、使用描述符设计

很多描述符的示例已经是Python的一部分。我们不需要重复属性、类方法或静态方法。

创建新的描述符最引人注目的情况是在Python和一些非Python之间建立映射关系。例如,对象-关系数据库映射,需要大量的维护以确保一个Python类有正确的字段以正确的顺序匹配一个SQL表和列。同样,当映射到一些Python之外,一个描述符类可以处理数据编码和解码或从外部获取数据来源。

当构建一个web服务端,我们可能会考虑使用描述符来做web服务请求。例如, __get__() 方法可能会变成一个HTTP的GET请求, __set__() 方法可能会变成一个HTTP的PUT请求。

在某些情况下,一个单一的请求可能填充几个描述符的数据。在这种情况下, __get__() 方法将检查实例的缓存以及在一个HTTP请求之前返回一个值。

通过属性来操作许多数据描述符操作会非常简单。这给我们提供开始的机会:首先编写属性。如果属性处理变得非常昂贵和复杂,则我们可以切换到描述符来对类进行重构。

3、展望未来

在下一章,我们将密切关注将在5、6、7章探索的 ABCs(抽象基类) 。这些基础知识将帮助我们定义类,它会与现有Python特性很好地集成。它们还将允许我们创建执行一致的设计和扩展的类层次结构。

正文到此结束
Loading...