python描述符协议

13 阅读4分钟

一、描述符协议简介

  1. 定义
    • 描述符是一种使用特殊方法(__get____set____delete__)来控制对属性访问的协议。描述符是一个包含这些特殊方法的类的实例,当通过属性访问语法(如obj.attr)访问属性时,Python会自动调用这些方法。
  2. 主要用途
    • 描述符可以用于实现属性的验证、缓存、通知等复杂行为。例如,你可以创建一个描述符来确保一个属性的值始终是正数,或者在属性值改变时触发某些操作。

二、描述符的分类

  1. 数据描述符
    • 数据描述符是同时定义了__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是一个数据描述符。它确保通过它定义的属性只能被设置为正整数。
  2. 非数据描述符
    • 非数据描述符只定义了__get__方法,没有定义__set__方法。它只覆盖了对属性的读取操作。当设置属性值时,会直接在实例的__dict__中设置,不会调用描述符的__set__方法(因为没有定义)。
    • 例如:
      class ReadOnly:
          def __init__(self, value):
              self.value = value
      
          def __get__(self, instance, owner):
              return self.value
      
      这里的ReadOnly是一个非数据描述符。它使得通过它定义的属性只能被读取,不能被设置。

三、描述符的工作原理

  1. 属性查找过程
    • 当访问一个属性时,Python会按照以下顺序查找:
      1. 在实例的__dict__中查找。
      2. 如果没有找到,检查类的__dict__中是否有数据描述符。
      3. 如果没有数据描述符,再次检查实例的__dict__
      4. 如果还是没有找到,检查类的__dict__中是否有非数据描述符。
      5. 如果都没有找到,抛出AttributeError
    • 例如,对于一个类A和它的实例a,如果A有一个数据描述符属性x,那么访问a.x时会直接调用描述符的__get__方法,而不是从a.__dict__中查找。
  2. 描述符的绑定
    • 描述符是绑定到类的,而不是实例的。这意味着多个实例共享同一个描述符实例。描述符通过__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...>
      
      在这个例子中,obj1obj2共享同一个描述符attr,但__get__方法可以根据instance参数返回不同的值。

四、使用描述符的场景和优势

  1. 验证属性值
    • 描述符可以方便地实现对属性值的验证逻辑。例如,确保一个属性的值是正数、字符串长度不超过某个限制等。
    • 例如:
      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
      
  2. 实现缓存
    • 描述符可以用于实现属性的缓存。当第一次访问属性时,计算其值并缓存起来,后续访问直接返回缓存的值。
    • 例如:
      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
      
  3. 通知属性变化
    • 描述符可以在属性值变化时触发通知或执行其他操作。例如,记录属性的修改次数、通知其他对象属性已更改等。
    • 例如:
      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的面向对象编程提供了更多的灵活性和功能。