Chapter 20 - Attribute Descriptors
描述符是对多个属性运用相同存取逻辑的一种方式。它是实现了特定协议的类,
property类就完整地实现了描述符协议。
20.1 描述符示例:验证属性
简单的描述符实现

- 描述符相关的定义:
- 描述符类:实现描述符协议的类;
- 托管类:将描述符实例声明为类属性的类;
- 储存属性:托管实例中存储自身托管属性的属性(实际的实例属性);
- 托管属性:托管类中由描述符实例处理的公开属性,值存储在储存属性中;
- 实现了
__get__、__set__、或__delete__方法的类是描述符(未实现的操作会忽略描述符,使用默认逻辑)。创建一个描述符实例,作为托管类的类属性,即可使用描述符; - 需要注意直接处理托管实例的
__dict__属性,避免触发__set__方法无限递归; - 注意:描述符的使用不会初始化储存属性。因此在下面的例子中,如果在
__init__中不初始化self.weight,则此时读取self.weight会得到Quantity对象;在初始化self.weight后,再读取self.weight则会得到对应的值(此时对于读取操作而言,实例属性覆盖了类属性,这是因为该操作没有被托管给描述符)。
class Quantity: # <1>
def __init__(self, storage_name):
self.storage_name = storage_name # <2>
def __set__(self, instance, value): # <3> 赋值托管属性
if value > 0:
instance.__dict__[self.storage_name] = value # <4>
else:
raise ValueError('value must be > 0')
class LineItem:
weight = Quantity('weight') # <5>
price = Quantity('price') # <6>
def __init__(self, description, weight, price): # <7>
...
自动生成储存属性的名称
- 可以在属性名中使用井号
#来避免与自定义属性名冲突:#符号是无效的 Python 句法,但getattr、setattr、__dict__均可以使用这样的属性名; - 可以使用
self.__class__获取实例所属的类引用,以得到各类属性,如__name__等; - 注意:在创建描述符实例时,还未完成托管类的定义,因此无法得到托管类的名称;
__get__方法有三个参数:描述符自身self、托管实例instance、托管类的引用owner。owner可以用于获取类属性等,默认为instance实例的类型。如果尝试用获取类属性的方式读取托管属性(LineItem.weight),则instance=None,此时应当返回描述符本身;- Django 模型的字段就是描述符。
20.2 覆盖型与非覆盖型描述符对比
描述符根据是否定义
__set__方法分为两大类:覆盖型描述符和非覆盖类描述符。
覆盖型描述符
- 实现
__set__方法覆盖了对实例属性的赋值操作,因此属于覆盖型描述符。可以不实现__get__方法,读操作会直接读实例属性(若不存在则为同名的描述符类属性); - 特性也是覆盖型描述符。
非覆盖型描述符
- 非覆盖型描述符没有实现
__set__方法,因此可以直接修改实例属性。同名的实例属性会覆盖描述符,从而导致描述符无法处理该属性; - 方法是以非覆盖型描述符实现的。
覆盖描述符
- 依附在类上的描述符无法控制为类属性赋值的操作;
- 可以通过为类属性赋值来覆盖描述符(猴子补丁);
- 读类属性的操作可用
__get__处理,而写类属性的操作不可用__set__处理。
20.3 方法是描述符
- 在类中定义的函数属于绑定方法。用户定义的函数对象都会含有特殊定义的
__get__方法,因此它的作用就是非覆盖型描述符; obj.method获取的是绑定方法对象,Class.method获取的是定义的函数对象本身。一个是方法(class 'method'),一个是函数(class 'function');- 绑定方法对象是可调用的对象,它储存了函数本身,并将托管实例绑定为函数的第一个参数(类似于
functools.partial); - 绑定方法对象有以下的属性:
__self__属性储存调用该方法的实例引用;__func__属性储存了托管类原始函数的引用;__call__方法实现了可调用,它会调用原始函数,将__self__作为第一个参数传入,用这种方式来隐式绑定形参self。
20.4 描述符用法建议
- 创建只读属性最简单的方法是使用特性;
- 只读描述符必须有
__set__方法,否则会被同名实例属性覆盖; - 用于验证的描述符可以只有
__set__方法用于设值; - 非覆盖型描述符可用于缓存结果:第一次访问该属性时通过描述符执行计算,计算后将结果保存到实例属性中,将描述符覆盖掉;
- 特殊方法不会被实例属性覆盖:解释器只会在类中寻找对应的特殊方法;
- 需要大量不受控的动态属性时,应当对动态属性进行过滤、转义,或通过类来直接使用类方法,以此维护数据健全。
20.5 描述符的文档字符串和覆盖删除操作
- 在描述符类中定义
__delete__方法即可处理删除托管属性。