Python深度解析:描述符的内部机制与高级用法

172 阅读10分钟

什么是描述符协议?

Python 的描述符协议是一种允许对象管理访问属性的特殊协议。描述符是实现了特定协议的对象,这个协议由两个基本方法组成:__get__, __set____delete__。这个协议允许你控制属性的访问、赋值和删除行为。

描述符通常用在类中,以提供一个可以重用的属性管理机制。它们通常用在实现属性装饰器或者类属性管理时。

这里是描述符协议的三个方法的简要说明:

  1. __get__(self, instance, owner) :

    • 作用:用于获取属性的值。此方法在访问属性时被调用,应该返回属性的值。

    • 参数解析

      • self: 描述符对象本身。
      • instance: 包含该属性的实例(即描述符所在类的实例),如果通过类来访问描述符属性,instance 就是 None。
      • owner: 包含该属性的类。owner拥有描述符的类本身
  2. __set__(self, instance, value) :

    • 作用:用于设置属性的值。此方法在设置属性时被调用,负责将值赋给属性。不返回任何值。

    • 参数解析

      • self: 描述符对象本身。
      • instance: 包含该属性的实例。
      • value: 要设置的值。
  3. __delete__(self, instance) :

    • 作用:用于删除属性。此方法在删除属性时被调用。不返回任何值。

    • 参数解析

      • self: 描述符对象本身。
      • instance: 包含该属性的实例。

描述符的类型:

数据描述符(Data Descriptor) :同时定义了 getset 方法(有时也包括 delete)。数据描述符具有更高的优先级。

非数据描述符(Non-Data Descriptor) :只定义了 get 方法。

如何定义和使用描述符

描述符可以定义在类中,这里是一个简单的例子:

class Descriptor:
    def __init__(self, initial_value=None):
        self.value = initial_value

    def __get__(self, instance, owner):
        print(f"get {self.value}")
        return self.value

    def __set__(self, instance, value):
        print(f"set {self.value}")
        self.value = value

    def __delete__(self, instance):
        print(f"delete {self.value}")
        del self.value

class MyClass:
    my_descriptor = Descriptor("Initial Value")

    def __init__(self, value):
        self.my_descriptor = value

obj = MyClass("New Value")
print("print " + obj.my_descriptor)  # 访问属性,调用 __get__
obj.my_descriptor = "Another Value"  # 赋值属性,调用 __set__
del obj.my_descriptor  # 删除属性,调用 __delete__

# set Initial Value
# get New Value
# printNew Value
# set New Value
# delete Another Value

在这个例子中,Descriptor 类实现了描述符协议,MyClass 使用了 Descriptor 作为类属性。通过 obj.my_descriptor 的访问、赋值和删除,展示了描述符协议的使用。

python中实现了描述符协议的内置函数

@property

描述符协议实际上是 property() 的底层机制,property() 是一种更简洁的方式来创建描述符。通过 property(),我们可以轻松定义属性的获取、设置和删除行为。

class MyClass:
    def __init__(self, value):
        self._value = value

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        if new_value < 0:
            raise ValueError("Value cannot be negative.")
        self._value = new_value

    @value.deleter
    def value(self):
        del self._value

obj = MyClass(10)
print(obj.value)  # 访问属性,输出10
obj.value = 5     # 设置属性
del obj.value     # 删除属性,输出5

静态函数装饰器

staticmethod 将一个方法转换为静态方法,不需要实例化类就可以调用。它只实现了 get 方法,因此是一个非数据描述符。

class MyClass:
    @staticmethod
    def my_static_method():
        print("This is a static method.")

MyClass.my_static_method()  # 调用时不需要实例

类函数装饰器

classmethod 将一个方法转换为类方法,调用时会将类本身作为第一个参数传递给方法。它实现了 get 方法,也是一个非数据描述符。

class MyClass:
    @classmethod
    def my_class_method(cls):
        print(f"This is a class method from {cls}.")

MyClass.my_class_method()  # 调用时传递类作为第一个参数

描述符协议实际使用场景

  1. 属性封装:描述Python中如何使用描述符来封装属性,防止直接访问或修改实例的某些属性。
  2. 类型检查:设计一个描述符,用于确保某个类的属性总是特定类型。如果尝试赋予该属性错误类型的值,描述符应该抛出TypeError
  3. 惰性属性计算:解释惰性计算的概念,并编写一个描述符,使得某个属性的计算仅在首次访问时执行,后续访问则直接返回已计算的值。
  4. 属性缓存:如何使用描述符来实现属性的缓存机制?请提供一个示例,其中某个资源密集型的属性仅计算一次,并在后续访问中复用该值。
  5. 只读属性:描述如何使用描述符创建一个只读属性。如果尝试修改这个属性,程序应该有什么行为?
  6. 属性访问日志:编写一个描述符,用于记录每次访问或修改某个属性的日志信息,包括访问或修改的时间和值。
  7. 方法装饰器:描述符可以与装饰器结合使用,请提供一个示例,展示如何使用描述符为类中的方法添加日志记录功能。
  8. 继承中的描述符:如果一个子类继承了一个带有描述符的属性,讨论子类如何能够重写或扩展这个属性的行为。
  9. 动态属性管理:编写一个描述符,允许动态地为类添加属性,并在访问这些属性时执行特定的逻辑。
  10. 线程安全:描述符是否可以用于实现线程安全的属性?如果可以,请简要说明如何实现。
  11. 描述符与元类:描述符和元类都可以用于自定义类的行为。讨论它们之间的差异,并给出使用描述符可能比元类更适用的场景。
  12. 属性依赖注入:如何使用描述符来实现属性的依赖注入,使得一个属性的值依赖于其他属性或外部资源?

描述符协议的坑点(注意点)

__get__ 方法中的返回值问题

  • 描述符的 __get__ 方法必须返回一个值。如果没有显式返回,Python 将返回 None,这可能会导致意想不到的行为。
  • 注意,如果你通过类而不是实例来访问描述符属性,instance 参数会是 None,需要在代码中处理这种情况。

数据描述符 vs. 非数据描述符

  • 数据描述符(定义了 __set____delete__ 方法)比非数据描述符(仅定义了 __get__ 方法)具有更高的优先级。当实例和类中都存在同名属性时,数据描述符会优先被调用。
  • 这个优先级机制可能会导致在实例上无法通过普通方式覆盖描述符。
  • 处理方式: 在使用描述符时,要清楚数据描述符和非数据描述符的优先级区别,并合理设计属性访问的逻辑。

描述符与继承

  • 如果你在子类中重载了父类中的描述符属性,可能会导致预期之外的行为。特别是在子类中直接操作父类描述符管理的属性时,继承结构可能导致访问不一致。
  • 在设计继承关系中的描述符时,确保理解描述符在继承链中的行为,避免在子类中不必要地覆盖父类描述符。

描述符和类变量

  • 描述符通常会在实例级别工作,但在类级别也可以访问。这意味着如果你通过类名访问描述符,描述符的 __get__ 方法的 instance 参数将是 None
  • 有时你可能会需要处理类变量和实例变量之间的关系,这需要在描述符中做适当的处理。
  • 处理方式: 在设计描述符时考虑类级别的访问,并在 __get__ 方法中处理 instance 参数为 None 的情况。

循环引用

  • 在描述符内部引用实例或其他属性时,如果不小心,可能会引入循环引用,这会导致内存泄漏。
  • Python 的垃圾回收机制依赖于引用计数,循环引用可能会导致对象无法被回收。
  • 处理方式: 尽量避免在描述符中直接持有对实例的强引用,或者在适当的时候使用 weakref 模块来管理对象的引用。

__delete__ 方法的使用

  • 很少有场景会用到描述符的 __delete__ 方法,但如果需要实现删除操作,务必确保删除逻辑的安全性和正确性。

描述符协议的实践

实现一个控制温度下限的装饰器

如华氏度中,温度不会低于-273.15华氏度,那么就可以通过描述符对温度的赋值进行限制

class Temperature:
    def __init__(self, value=0):
        self.value = value

    def __get__(self, instance, owner):
        return self.value

    def __set__(self, instance, value):
        if value < -273.15:
            raise ValueError("Temperature below -273.15 is not possible.")
        self.value = value

class WeatherStation:
    temperature = Temperature()

station = WeatherStation()
station.temperature = 20
print(station.temperature)  # 输出: 20
try:
    station.temperature = -300  # 抛出 ValueError
except ValueError as e:
    print("exception happend: " + str(e))

# 20
# exception happend: Temperature below -273.15 is not possible.

实现一个单例模式

使用描述符实现一个单例模式的类,确保这个类只能有一个实例,并从所有尝试创建新实例的地方返回同一个实例。

class Singleton:
    def __init__(self, cls):
        self.cls = cls
        self.instance = None

    def __get__(self, instance, owner):
        if self.instance is None:
            self.instance = self.cls()
        return self.instance

# 使用单例模式描述符
class MyClass:
    _singleton = Singleton(object)

    def __init__(self):
        self.data = "I am the only instance"

    @staticmethod
    def instance():
        return MyClass._singleton

# 测试单例模式
obj1 = MyClass.instance()
obj2 = MyClass.instance()

print(obj1 is obj2)  # 输出: True (两个对象是相同的实例)
print(obj1.data)     # 输出: I am the only instance
print(obj2.data)     # 输出: I am the only instance

实现内置的@property

内置的property函数是如何利用描述符来实现的?请提供一个自定义的property实现。

class MyProperty:
    def __init__(self, fget=None, fset=None, fdel=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel

    def __get__(self, instance, owner):
        print("trigger __get__")
        if instance is None:
            return self
        if self.fget is None:
            raise AttributeError("Unreadable attribute")
        return self.fget(instance)

    def __set__(self, instance, value):
        print("trigger __set__")
        if self.fset is None:
            raise AttributeError("Can't set attribute")
        self.fset(instance, value)

    def __delete__(self, instance):
        print("trigger __delete__")
        if self.fdel is None:
            raise AttributeError("Can't delete attribute")
        self.fdel(instance)

    def getter(self, fget):
        print("register getter")
        self.fget = fget
        return self

    def setter(self, fset):
        print("register setter")
        self.fset = fset
        return self

    def deleter(self, fdel):
        print("register deleter")
        self.fdel = fdel
        return self

# 使用自定义的 @property
class MyClass:
    def __init__(self, value):
        self._value = value

    @MyProperty
    def value(self):
        return self._value

    @value.setter
    def value(self, new_value):
        if new_value < 0:
            raise ValueError("Value cannot be negative.")
        self._value = new_value

    @value.deleter
    def value(self):
        del self._value

# 测试自定义的 @property
obj = MyClass(10)
print(obj.value)  # 调用自定义的 __get__
obj.value = 20    # 调用自定义的 __set__
print(obj.value)  # 再次调用 __get__
del obj.value     # 调用自定义的 __delete__

# register setter
# register deleter
# trigger __get__
# 10
# trigger __set__
# trigger __get__
# 20
# trigger __delete__

实现静态方法与类方法的行为

描述符如何帮助实现静态方法和类方法的行为?请解释它们的内部机制。

class MyStaticMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        return self.func
    
class MyClassMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        def bound_func(*args, **kwargs):
            return self.func(owner, *args, **kwargs)
        return bound_func

# 测试自定义静态方法描述符
class MyClass:
    class_attribute = "I am a class attribute"

    @MyStaticMethod
    def my_static_method():
        print("This is a custom static method. ")

    @MyClassMethod
    def my_class_method(cls):
        print(f"This is a custom class method of {cls}")
        print(f"Class attribute: {cls.class_attribute}")
    
    def my_instance_method(self):
        print(f"This is a custom instance method of {self}")
        print(f"Class attribute: {self.class_attribute}")

# 测试静态方法
MyClass.my_static_method()  # 输出: This is a custom static method.
obj = MyClass()
obj.my_static_method()      # 输出: This is a custom static method.

# 测试类方法
MyClass.my_class_method()  # 输出: This is a custom class method of <class '__main__.MyClass'>
                           #      Class attribute: I am a class attribute
obj = MyClass()
obj.my_class_method()      # 输出: This is a custom class method of <class '__main__.MyClass'>
                           #      Class attribute: I am a class attribute

实现一个具有缓存功能的装饰器

class LazyProperty:
    def __init__(self, func):
        self.func = func
        self.value = None

    def __get__(self, instance, owner):
        if self.value is None:
            self.value = self.func(instance)
        return self.value

class MyClass:
    @LazyProperty
    def expensive_computation(self):
        print("Computing...")
        return 42

obj = MyClass()
print(obj.expensive_computation)  # 第一次调用时执行计算并缓存结果
print(obj.expensive_computation)  # 第二次调用时直接返回缓存结果

# 输出结果
# Computing...
# 42
# 42