一、描述符协议简介
- 定义
- 描述符是一种使用特殊方法(
__get__
、__set__
和__delete__
)来控制对属性访问的协议。描述符是一个包含这些特殊方法的类的实例,当通过属性访问语法(如obj.attr
)访问属性时,Python会自动调用这些方法。
- 描述符是一种使用特殊方法(
- 主要用途
- 描述符可以用于实现属性的验证、缓存、通知等复杂行为。例如,你可以创建一个描述符来确保一个属性的值始终是正数,或者在属性值改变时触发某些操作。
二、描述符的分类
- 数据描述符
- 数据描述符是同时定义了
__get__
和__set__
方法的描述符。它覆盖了对属性的读取和写入操作。当访问属性时,会调用__get__
方法;当设置属性值时,会调用__set__
方法。 - 例如:
在这个例子中,class PositiveInteger: def __init__(self, name): self.name = name def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, int) or value <= 0: raise ValueError("Value must be a positive integer") instance.__dict__[self.name] = value
PositiveInteger
是一个数据描述符。它确保通过它定义的属性只能被设置为正整数。
- 数据描述符是同时定义了
- 非数据描述符
- 非数据描述符只定义了
__get__
方法,没有定义__set__
方法。它只覆盖了对属性的读取操作。当设置属性值时,会直接在实例的__dict__
中设置,不会调用描述符的__set__
方法(因为没有定义)。 - 例如:
这里的class ReadOnly: def __init__(self, value): self.value = value def __get__(self, instance, owner): return self.value
ReadOnly
是一个非数据描述符。它使得通过它定义的属性只能被读取,不能被设置。
- 非数据描述符只定义了
三、描述符的工作原理
- 属性查找过程
- 当访问一个属性时,Python会按照以下顺序查找:
- 在实例的
__dict__
中查找。 - 如果没有找到,检查类的
__dict__
中是否有数据描述符。 - 如果没有数据描述符,再次检查实例的
__dict__
。 - 如果还是没有找到,检查类的
__dict__
中是否有非数据描述符。 - 如果都没有找到,抛出
AttributeError
。
- 在实例的
- 例如,对于一个类
A
和它的实例a
,如果A
有一个数据描述符属性x
,那么访问a.x
时会直接调用描述符的__get__
方法,而不是从a.__dict__
中查找。
- 当访问一个属性时,Python会按照以下顺序查找:
- 描述符的绑定
- 描述符是绑定到类的,而不是实例的。这意味着多个实例共享同一个描述符实例。描述符通过
__get__
方法的instance
参数来区分不同的实例。 - 例如:
在这个例子中,class Descriptor: def __get__(self, instance, owner): if instance is None: return self return f"Value for {instance}" class MyClass: attr = Descriptor() obj1 = MyClass() obj2 = MyClass() print(obj1.attr) # 输出:Value for <__main__.MyClass object at 0x...> print(obj2.attr) # 输出:Value for <__main__.MyClass object at 0x...>
obj1
和obj2
共享同一个描述符attr
,但__get__
方法可以根据instance
参数返回不同的值。
- 描述符是绑定到类的,而不是实例的。这意味着多个实例共享同一个描述符实例。描述符通过
四、使用描述符的场景和优势
- 验证属性值
- 描述符可以方便地实现对属性值的验证逻辑。例如,确保一个属性的值是正数、字符串长度不超过某个限制等。
- 例如:
class PositiveInteger: def __init__(self, name): self.name = name def __get__(self, instance, owner): return instance.__dict__[self.name] def __set__(self, instance, value): if not isinstance(value, int) or value <= 0: raise ValueError("Value must be a positive integer") instance.__dict__[self.name] = value class MyClass: attr = PositiveInteger("attr") obj = MyClass() obj.attr = 5 # 正常设置 obj.attr = -1 # 抛出 ValueError
- 实现缓存
- 描述符可以用于实现属性的缓存。当第一次访问属性时,计算其值并缓存起来,后续访问直接返回缓存的值。
- 例如:
class CachedProperty: def __init__(self, func): self.func = func self.cache = {} def __get__(self, instance, owner): if instance not in self.cache: self.cache[instance] = self.func(instance) return self.cache[instance] class MyClass: def __init__(self, value): self._value = value @CachedProperty def value(self): print("Computing value...") return self._value * 2 obj = MyClass(5) print(obj.value) # 输出:Computing value... 10 print(obj.value) # 输出:10
- 通知属性变化
- 描述符可以在属性值变化时触发通知或执行其他操作。例如,记录属性的修改次数、通知其他对象属性已更改等。
- 例如:
class NotifyDescriptor: def __init__(self, name): self.name = name self._value = None def __get__(self, instance, owner): return self._value def __set__(self, instance, value): print(f"Setting {self.name} to {value}") self._value = value class MyClass: attr = NotifyDescriptor("attr") obj = MyClass() obj.attr = 10 # 输出:Setting attr to 10 obj.attr = 20 # 输出:Setting attr to 20
五、总结
描述符协议是Python中一个强大的功能,它允许开发者通过定义特殊的__get__
、__set__
和__delete__
方法来控制属性的访问和修改。描述符分为数据描述符和非数据描述符,它们在属性查找和绑定过程中有着不同的行为。描述符可以用于验证属性值、实现缓存、通知属性变化等多种场景,为Python的面向对象编程提供了更多的灵活性和功能。