描述符是对多个属性运用相同存取逻辑的一种方式。例如,Django ORM 和 SQL Alchemy
等 ORM 中的字段类型是描述符,把数据库记录中字段里的数据与 Python 对象的属性对应
起来。
描述符是实现了特定协议的类,这个协议包括 __get__、__set__ 和 __delete__ 方
法。property 类实现了完整的描述符协议。通常,可以只实现部分协议。其实,我们在
真实的代码中见到的大多数描述符只实现了 __get__ 和 __set__ 方法,还有很多只实现
了其中的一个。
描述符是 Python 的独有特征,不仅在应用层中使用,在语言的基础设施中也有用到。除
了特性之外,使用描述符的 Python 功能还有方法及 classmethod 和 staticmethod 装饰
器。理解描述符是精通 Python 的关键。本章的话题就是描述符。
实现了 __get__、__set__ 或 __delete__ 方法的类是描述符。描述符的用法是,创建
一个实例,作为另一个类的类属性。
class Quantity: def __init__(self, storage_name): self.storage_name = storage_name def __set__(self, instance, value): if value > 0: instance.__dict__[self.storage_name] = value else: raise ValueError('value must be > 0') class LineItem: weight = Quantity('weight') price = Quantity('price') def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
改进版
class Quantity: __counter = 0 def __init__(self): cls = self.__class__ prefix = cls.__name__ index = cls.__counter self.storage_name = '_{}#{}'.format(prefix, index) cls.__counter += 1 def __get__(self, instance, owner): return getattr(instance, self.storage_name) def __set__(self, instance, value): if value > 0: setattr(instance, self.storage_name, value) else: raise ValueError('value must be > 0') class LineItem: weight = Quantity() price = Quantity() def __init__(self, description, weight, price): self.description = description self.weight = weight self.price = price def subtotal(self): return self.weight * self.price
使用特性工厂函数实现
def quantity(): try: quantity.counter += 1 except AttributeError: quantity.counter = 0 storage_name = '_{}:{}'.format('quantity', quantity.counter) def qty_getter(instance): return getattr(instance, storage_name) def qty_setter(instance, value): if value > 0: setattr(instance, storage_name, value) else: raise ValueError('value must be > 0') return property(qty_getter, qty_setter)
更深入的例子
class AutoStorage:#最基础的类,负责存储属性获取属性值和设置属性值 __counter = 0 def __init__(self): cls = self.__class__ prefix = cls.__name__ index = cls.__counter self.storage_name = '_{}#{}'.format(prefix, index) cls.__counter += 1 def __get__(self, instance, owner): if instance is None: return self else: return getattr(instance, self.storage_name) def __set__(self, instance, value): setattr(instance, self.storage_name, value) class Validated(abc.ABC, AutoStorage):#抽象基类,子类必须实现validate方法,用来验证设置值是否有效 def __set__(self, instance, value): value = self.validate(instance, value) super().__set__(instance, value) @abc.abstractmethod def validate(self, instance, value): """return validated value or raise ValueError""" class Quantity(Validated):# 验证设置值是否大于0的验证类 def validate(self, instance, value): if value <= 0: raise ValueError('value must be > 0') return value class NonBlank(Validated):# 验证设置值是否为空的验证类 def validate(self, instance, value): value = value.strip() if len(value) == 0: raise ValueError('value cannot be empty or blank') return value
覆盖型描述符
实现 __set__ 方法的描述符属于覆盖型描述符,因为虽然描述符是类属性,但是实现
__set__ 方法的话,会覆盖对实例属性的赋值操作
特性也
是覆盖型描述符:如果没提供设值函数,property 类中的 __set__ 方法会抛出
AttributeError 异常,指明那个属性是只读的。
没有 __get__ 方法的覆盖型描述符
通常,覆盖型描述符既会实现 __set__ 方法,也会实现 __get__ 方法,不过也可以只实
现 __set__ 方法,如示例 20-1 所示。此时,只有写操作由描述符处理。通过实例读取描
述符会返回描述符对象本身,因为没有处理读操作的 __get__ 方法。如果直接通过实例
的 __dict__ 属性创建同名实例属性,以后再设置那个属性时,仍会由 __set__ 方法插
手接管,但是读取那个属性的话,就会直接从实例中返回新赋予的值,而不会返回描述符
对象。也就是说,实例属性会遮盖描述符,不过只有读操作是如此
非覆盖型描述符
没有实现 __set__ 方法的描述符是非覆盖型描述符。如果设置了同名的实例属性,描述
符会被遮盖,致使描述符无法处理那个实例的那个属性。方法是以非覆盖型描述符实现
的
描述符用法建议
使用特性以保持简单
内置的 property 类创建的其实是覆盖型描述符,__set__ 方法和 __get__ 方法都
实现了,即便不定义设值方法也是如此。特性的 __set__ 方法默认抛出
AttributeError: can't set attribute,因此创建只读属性最简单的方式是使用特
性,这能避免下一条所述的问题。
只读描述符必须有 __set__ 方法
如果使用描述符类实现只读属性,要记住,__get__ 和 __set__ 两个方法必须都定
义,否则,实例的同名属性会遮盖描述符。只读属性的 __set__ 方法只需抛出
AttributeError 异常,并提供合适的错误消息。
用于验证的描述符可以只有 __set__ 方法
对仅用于验证的描述符来说,__set__ 方法应该检查 value 参数获得的值,如果有
效,使用描述符实例的名称为键,直接在实例的 __dict__ 属性中设置。这样,从实例中
读取同名属性的速度很快,因为不用经过 __get__ 方法处理
仅有 __get__ 方法的描述符可以实现高效缓存
如果只编写了 __get__ 方法,那么创建的是非覆盖型描述符。这种描述符可用于执
行某些耗费资源的计算,然后为实例设置同名属性,缓存结果。同名实例属性会遮盖描述
符,因此后续访问会直接从实例的 __dict__ 属性中获取值,而不会再触发描述符的
__get__ 方法。
非特殊的方法可以被实例属性遮盖
由于函数和方法只实现了 __get__ 方法,它们不会处理同名实例属性的赋值操作。
因此,像 my_obj.the_method = 7 这样简单赋值之后,后续通过该实例访问
the_method 得到的是数字 7——但是不影响类或其他实例。然而,特殊方法不受这个问
题的影响。解释器只会在类中寻找特殊的方法,也就是说,repr(x) 执行的其实是
x.__class__.__repr__(x),因此 x 的 __repr__ 属性对 repr(x) 方法调用没有影响。
出于同样的原因,实例的 __getattr__ 属性不会破坏常规的属性访问规则。