3.3.2. 自定义属性访问

可以定义下列方法来自定义对类实例属性访问(x.name 的使用、赋值或删除)的具体含义.

  • object.getattr(self, name)
  • 当默认属性访问因引发 AttributeError 而失败时被调用 (可能是调用 getattribute() 时由于 name 不是一个实例属性或 self 的类关系树中的属性而引发了 AttributeError;或者是对 name 特性属性调用 get() 时引发了 AttributeError)。此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。

请注意如果属性是通过正常机制找到的,getattr() 就不会被调用。(这是在 getattr()setattr() 之间故意设置的不对称性。)这既是出于效率理由也是因为不这样设置的话 getattr() 将无法访问实例的其他属性。要注意至少对于实例变量来说,你不必在实例属性字典中插入任何值(而是通过插入到其他对象)就可以模拟对它的完全控制。请参阅下面的 getattribute() 方法了解真正获取对属性访问的完全控制权的办法。

  • object.getattribute(self, name)
  • 此方法会无条件地被调用以实现对类实例属性的访问。如果类还定义了 getattr(),则后者不会被调用,除非 getattribute() 显式地调用它或是引发了 AttributeError。此方法应当返回(找到的)属性值或是引发一个 AttributeError 异常。为了避免此方法中的无限递归,其实现应该总是调用具有相同名称的基类方法来访问它所需要的任何属性,例如 object.getattribute(self, name)

注解

此方法在作为通过特定语法或内置函数隐式地调用的结果的情况下查找特殊方法时仍可能会被跳过。参见 特殊方法查找

  • object.setattr(self, name, value)
  • 此方法在一个属性被尝试赋值时被调用。这个调用会取代正常机制(即将值保存到实例字典)。 name 为属性名称, value 为要赋给属性的值。

如果 setattr() 想要赋值给一个实例属性,它应该调用同名的基类方法,例如 object.setattr(self, name, value)

  • object.delattr(self, name)
  • 类似于 setattr() 但其作用为删除而非赋值。此方法应该仅在 del obj.name 对于该对象有意义时才被实现。

  • object.dir(self)

  • 此方法会在对相应对象调用 dir() 时被调用。返回值必须为一个序列。 dir() 会把返回的序列转换为列表并对其排序。

3.3.2.1. 自定义模块属性访问

特殊名称 getattrdir 还可被用来自定义对模块属性的访问。模块层级的 getattr 函数应当接受一个参数,其名称为一个属性名,并返回计算结果值或引发一个 AttributeError。如果通过正常查找即 object.getattribute() 未在模块对象中找到某个属性,则 getattr 会在模块的 dict 中查找,未找到时会引发一个 AttributeError。如果找到,它会以属性名被调用并返回结果值。

dir 函数应当不接受任何参数,并且返回一个表示模块中可访问名称的字符串序列。 此函数如果存在,将会重载一个模块中的标准 dir() 查找。

想要更细致地自定义模块的行为(设置属性和特性属性等待),可以将模块对象的 class 属性设置为一个 types.ModuleType 的子类。例如:

  1. import sys
  2. from types import ModuleType
  3.  
  4. class VerboseModule(ModuleType):
  5. def __repr__(self):
  6. return f'Verbose {self.__name__}'
  7.  
  8. def __setattr__(self, attr, value):
  9. print(f'Setting {attr}...')
  10. super().__setattr__(attr, value)
  11.  
  12. sys.modules[__name__].__class__ = VerboseModule

注解

定义模块的 getattr 和设置模块的 class 只会影响使用属性访问语法进行的查找 — 直接访问模块全局变量(不论是通过模块内的代码还是通过对模块全局字典的引用)是不受影响的。

在 3.5 版更改: class 模块属性改为可写。

3.7 新版功能: getattrdir 模块属性。

参见

  • PEP 562 - 模块 getattrdir
  • 描述用于模块的 getattrdir 函数。

3.3.2.2. 实现描述器

以下方法仅当一个包含该方法的类(称为 描述器 类)的实例出现于一个 所有者 类中的时候才会起作用(该描述器必须在所有者类或其某个上级类的字典中)。在以下示例中,“属性”指的是名称为所有者类 dict 中的特征属性的键名的属性。

  • object.get(self, instance, owner=None)
  • 调用此方法以获取所有者类的属性(类属性访问)或该类的实例的属性(实例属性访问)。 可选的 owner 参数是所有者类而 instance 是被用来访问属性的实例,如果通过 owner 来访问属性则返回 None

此方法应当返回计算得到的属性值或是引发 AttributeError 异常。

PEP 252 指明 get() 为带有一至二个参数的可调用对象。 Python 自身内置的描述器支持此规格定义;但是,某些第三方工具可能要求必须带两个参数。 Python 自身的 getattribute() 实现总是会传入两个参数,无论它们是否被要求提供。

  • object.set(self, instance, value)
  • 调用此方法以设置 instance 指定的所有者类的实例的属性为新值 value

请注意,添加 set()delete() 会将描述器变成“数据描述器”。 更多细节请参阅 发起调用描述器

  • object.delete(self, instance)
  • 调用此方法以删除 instance 指定的所有者类的实例的属性。

  • object.set_name(self, owner, name)

  • 在所有者类 owner 创建时被调用。描述器会被赋值给 name

3.6 新版功能.

属性 objclass 会被 inspect 模块解读为指定此对象定义所在的类(正确设置此属性有助于动态类属性的运行时内省)。对于可调用对象来说,它可以指明预期或要求提供一个特定类型(或子类)的实例作为第一个位置参数(例如,CPython 会为实现于 C 中的未绑定方法设置此属性)。

3.3.2.3. 发起调用描述器

总的说来,描述器就是具有“绑定行为”的对象属性,其属性访问已被描述器协议中的方法所重载,包括 get(), set()delete()。如果一个对象定义了以上方法中的任意一个,它就被称为描述器。

属性访问的默认行为是从一个对象的字典中获取、设置或删除属性。例如,a.x 的查找顺序会从 a.dict['x'] 开始,然后是 type(a).dict['x'],接下来依次查找 type(a) 的上级基类,不包括元类。

但是,如果找到的值是定义了某个描述器方法的对象,则 Python 可能会重载默认行为并转而发起调用描述器方法。这具体发生在优先级链的哪个环节则要根据所定义的描述器方法及其被调用的方式来决定。

描述器发起调用的开始点是一个绑定 a.x。参数的组合方式依 a 而定:

  • 直接调用
  • 最简单但最不常见的调用方式是用户代码直接发起调用一个描述器方法: x.get(a)

  • 实例绑定

  • 如果绑定到一个对象实例,a.x 会被转换为调用: type(a).dict['x'].get(a, type(a))

  • 类绑定

  • 如果绑定到一个类,A.x 会被转换为调用: A.dict['x'].get(None, A)

  • 超绑定

  • 如果 asuper 的一个实例,则绑定 super(B, obj).m() 会在 obj.class.mro 中搜索 B 的直接上级基类 A 然后通过以下调用发起调用描述器: A.dict['m'].get(obj, obj.class)

对于实例绑定,发起描述器调用的优先级取决于定义了哪些描述器方法。一个描述器可以定义 get()set()delete() 的任意组合。如果它没有定义 get(),则访问属性会返回描述器对象自身,除非对象的实例字典中有相应属性值。如果描述器定义了 set() 和/或 delete(),则它是一个数据描述器;如果以上两个都未定义,则它是一个非数据描述器。通常,数据描述器会同时定义 get()set(),而非数据描述器只有 get() 方法。定义了 set()get() 的数据描述器总是会重载实例字典中的定义。与之相对的,非数据描述器可被实例所重载。

Python 方法 (包括 staticmethod()classmethod()) 都是作为非描述器来实现的。因此实例可以重定义并重载方法。这允许单个实例获得与相同类的其他实例不一样的行为。

property() 函数是作为数据描述器来实现的。因此实例不能重载特性属性的行为。

3.3.2.4. slots

slots 允许我们显式地声明数据成员(例如特征属性)并禁止创建 dictweakref (除非是在 slots 中显式地声明或是在父类中可用。)

相比使用 dict 此方式可以显著地节省空间。 属性查找速度也可得到显著的提升。

  • object.slots
  • 这个类变量可赋值为字符串、可迭代对象或由实例使用的变量名构成的字符串序列。 slots 会为已声明的变量保留空间,并阻止自动为每个实例创建 dictweakref
3.3.2.4.1. 使用 slots 的注意事项
  • 当继承自一个未定义 slots 的类时,实例的 dictweakref 属性将总是可访问。

  • 没有 dict 变量,实例就不能给未在 slots 定义中列出的新变量赋值。尝试给一个未列出的变量名赋值将引发 AttributeError。新变量需要动态赋值,就要将 'dict' 加入到 slots 声明的字符串序列中。

  • 如果未给每个实例设置 weakref 变量,定义了 slots 的类就不支持对其实际的弱引用。如果需要弱引用支持,就要将 'weakref' 加入到 slots 声明的字符串序列中。

  • slots 是通过为每个变量名创建描述器 (实现描述器) 在类层级上实现的。因此,类属性不能被用来为通过 slots 定义的实例变量设置默认值;否则,类属性就会覆盖描述器赋值。

  • slots 声明的作用不只限于定义它的类。在父类中声明的 slots 在其子类中同样可用。不过,子类将会获得 dictweakref 除非它们也定义了 slots (其中应该仅包含对任何 额外 名称的声明位置)。

  • 如果一个类定义的位置在某个基类中也有定义,则由基类位置定义的实例变量将不可访问(除非通过直接从基类获取其描述器的方式)。这会使得程序的含义变成未定义。未来可能会添加一个防止此情况的检查。

  • 非空的 slots 不适用于派生自“可变长度”内置类型例如 intbytestuple 的派生类。

  • 任何非字符串可迭代对象都可以被赋值给 slots。映射也可以被使用;不过,未来可能会分别赋给每个键具有特殊含义的值。

  • class 赋值仅在两个类具有相同的 slots 时才会起作用。

  • 带有多个父类声明位置的多重继承也是可用的,但仅允许一个父类具有由声明位置创建的属性(其他基类必须具有空的位置布局) —— 违反规则将引发 TypeError

  • 如果为 slots 使用了一个迭代器,则会为迭代器的每个值创建描述器。 但是 slots 属性将为一个空迭代器。