Python中的描述符协议(Descriptor Protocol):理解`__get__`、`__set__`、`__delete__`的调用顺序

好的,下面我们深入探讨Python中的描述符协议。

Python描述符协议:理解__get____set____delete__的调用顺序

今天我们要深入探讨Python中一个非常强大但有时令人困惑的特性:描述符协议(Descriptor Protocol)。 掌握描述符是成为Python高级程序员的关键一步,它允许你控制属性访问,实现数据验证,创建只读属性,等等。 我们将详细了解__get____set____delete__方法,以及它们在不同场景下的调用顺序。

什么是描述符?

简单来说,描述符是一个实现了描述符协议的Python对象。描述符协议定义了__get____set____delete__这三个特殊方法。当对象的属性被访问、设置或删除时,如果该属性是一个描述符,那么这些特殊方法就会被调用。

描述符协议方法

  • __get__(self, instance, owner): 访问属性时调用。

    • self: 描述符实例本身。
    • instance: 拥有者类的实例。如果通过类访问描述符,则instanceNone
    • owner: 拥有者类本身。
  • __set__(self, instance, value): 设置属性时调用。

    • self: 描述符实例本身。
    • instance: 拥有者类的实例。
    • value: 要设置的值。
  • __delete__(self, instance): 删除属性时调用。

    • self: 描述符实例本身。
    • instance: 拥有者类的实例。

描述符的种类:数据描述符与非数据描述符

描述符可以分为两种类型:

  • 数据描述符 (Data Descriptor): 同时定义了__get____set__方法的描述符。
  • 非数据描述符 (Non-Data Descriptor): 只定义了__get__方法的描述符。

数据描述符和非数据描述符的区别在于,当属性查找时,它们优先级不同。 数据描述符的优先级高于实例字典,而非数据描述符的优先级低于实例字典。 这意味着,如果一个属性同时存在于实例字典和一个数据描述符中,数据描述符会覆盖实例字典中的值。 但如果是一个非数据描述符,实例字典中的值会覆盖非数据描述符。

调用顺序详解与代码示例

理解__get____set____delete__的调用顺序至关重要。让我们通过一些例子来详细说明。

1. 简单的非数据描述符

class MyDescriptor:
    def __get__(self, instance, owner):
        print("Getting attribute")
        if instance is None:
            return self  # 通过类访问
        return instance._my_attribute  # 通过实例访问

class MyClass:
    my_attribute = MyDescriptor()  # 描述符实例

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

# 访问
obj = MyClass(10)
print(obj.my_attribute)       # 输出: Getting attribute n 10
print(MyClass.my_attribute)   # 输出: Getting attribute n <__main__.MyDescriptor object at ...>

# 设置
obj.my_attribute = 20
print(obj.my_attribute) # Getting attribute n 10  <--- 描述符没有__set__,所以直接设置了实例的__dict__
print(obj.__dict__) # {'_my_attribute': 10, 'my_attribute': 20}

del obj.my_attribute # AttributeError: __delete__

在这个例子中,MyDescriptor只定义了__get__方法,因此它是一个非数据描述符。当通过实例访问my_attribute时,__get__被调用。但是,由于没有__set__方法,obj.my_attribute = 20 直接在实例的__dict__中创建了一个新的my_attribute属性,覆盖了描述符。 del obj.my_attribute 也会直接操作实例字典,删除实例字典中的my_attribute,而不是调用描述符的__delete__(因为根本没有定义)。

2. 数据描述符

class MyDescriptor:
    def __get__(self, instance, owner):
        print("Getting attribute")
        return instance._my_attribute

    def __set__(self, instance, value):
        print("Setting attribute to", value)
        instance._my_attribute = value

    def __delete__(self, instance):
        print("Deleting attribute")
        del instance._my_attribute

class MyClass:
    my_attribute = MyDescriptor()

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

# 访问
obj = MyClass(10)
print(obj.my_attribute)  # 输出: Getting attribute n 10

# 设置
obj.my_attribute = 20  # 输出: Setting attribute to 20
print(obj.my_attribute)  # 输出: Getting attribute n 20
print(obj.__dict__) # {'_my_attribute': 20}

# 删除
del obj.my_attribute  # 输出: Deleting attribute
print(obj.__dict__)  # {}

现在,MyDescriptor同时定义了__get____set____delete__,因此它是一个数据描述符。 当访问、设置或删除my_attribute时,相应的描述符方法都会被调用。 重要的是,即使你尝试直接在实例的__dict__中设置my_attribute,描述符仍然会拦截并处理该操作。

3. 通过类访问描述符

class MyDescriptor:
    def __get__(self, instance, owner):
        print(f"Getting attribute from class {owner.__name__}")
        if instance is None:
            return self
        return instance._my_attribute

    def __set__(self, instance, value):
        print(f"Setting attribute in instance of {instance.__class__.__name__} to", value)
        instance._my_attribute = value

    def __delete__(self, instance):
        print(f"Deleting attribute from instance of {instance.__class__.__name__}")
        del instance._my_attribute

class MyClass:
    my_attribute = MyDescriptor()

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

print("通过类访问:")
print(MyClass.my_attribute)

obj = MyClass(10)

print("n通过实例访问:")
print(obj.my_attribute)

print("n设置实例属性:")
obj.my_attribute = 20

print("n删除实例属性:")
del obj.my_attribute

在这个例子中,当通过类MyClass访问my_attribute时,__get__方法被调用,并且instance参数为Noneowner参数为MyClass。 描述符通常返回自身,或者执行其他与类相关的操作。

4. 只读属性

class ReadOnlyDescriptor:
    def __get__(self, instance, owner):
        print("Getting read-only attribute")
        return instance._my_attribute

    def __set__(self, instance, value):
        raise AttributeError("Cannot set read-only attribute")

    def __delete__(self, instance):
        raise AttributeError("Cannot delete read-only attribute")

class MyClass:
    read_only_attribute = ReadOnlyDescriptor()

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

obj = MyClass(10)
print(obj.read_only_attribute)

try:
    obj.read_only_attribute = 20
except AttributeError as e:
    print(e)

try:
    del obj.read_only_attribute
except AttributeError as e:
    print(e)

通过在__set____delete__方法中引发AttributeError,我们可以创建一个只读属性。任何尝试设置或删除该属性的操作都会导致异常。

5. 验证属性

class ValidatedDescriptor:
    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise TypeError("Value must be an integer")
        instance._my_attribute = value

    def __get__(self, instance, owner):
        print("Getting validated attribute")
        return instance._my_attribute

class MyClass:
    validated_attribute = ValidatedDescriptor()

    def __init__(self):
        pass

obj = MyClass()

try:
    obj.validated_attribute = "abc"
except TypeError as e:
    print(e)

obj.validated_attribute = 123
print(obj.validated_attribute)

在这个例子中,ValidatedDescriptor__set__方法中验证值的类型。如果值不是整数,则会引发TypeError

6. 描述符与继承

当涉及到继承时,描述符的行为可能会变得稍微复杂。 重要的是要理解描述符是在类级别定义的,并且会被该类的所有实例共享。 如果子类没有覆盖描述符属性,则它将继承父类的描述符行为。

class ParentDescriptor:
    def __get__(self, instance, owner):
        print("Getting attribute from ParentDescriptor")
        return instance._my_attribute

    def __set__(self, instance, value):
        print("Setting attribute in ParentDescriptor to", value)
        instance._my_attribute = value

class ParentClass:
    my_attribute = ParentDescriptor()

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

class ChildClass(ParentClass):
    pass

obj = ChildClass(10)
print(obj.my_attribute)
obj.my_attribute = 20
print(obj.my_attribute)

在这个例子中,ChildClass继承了ParentClassmy_attribute描述符。 因此,对ChildClass实例的my_attribute的访问和设置仍然会调用ParentDescriptor的方法。

7. 覆盖描述符

如果子类想要改变描述符的行为,它可以简单地覆盖父类的描述符属性。

class ChildDescriptor(ParentDescriptor):
    def __get__(self, instance, owner):
        print("Getting attribute from ChildDescriptor")
        return super().__get__(instance, owner)  # 调用父类的__get__

class ChildClass(ParentClass):
    my_attribute = ChildDescriptor()

obj = ChildClass(10)
print(obj.my_attribute)  # 输出: Getting attribute from ChildDescriptor n Getting attribute from ParentDescriptor n 10

在这个例子中,ChildClass使用一个新的描述符ChildDescriptor覆盖了ParentClassmy_attributeChildDescriptor可以有自己的逻辑,并且可以选择调用父类的__get__方法。

8. 使用property作为描述符的简化方式

Python内置的property函数是创建描述符的一种更简洁的方式,特别是对于简单的属性访问控制。property函数接受fget(获取属性)、fset(设置属性)和fdel(删除属性)函数作为参数,并返回一个描述符对象。

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

    def get_my_attribute(self):
        print("Getting attribute using property")
        return self._my_attribute

    def set_my_attribute(self, value):
        print("Setting attribute using property to", value)
        self._my_attribute = value

    def del_my_attribute(self):
        print("Deleting attribute using property")
        del self._my_attribute

    my_attribute = property(get_my_attribute, set_my_attribute, del_my_attribute)

obj = MyClass(10)
print(obj.my_attribute)
obj.my_attribute = 20
del obj.my_attribute

property本质上是描述符协议的一个具体应用,它简化了创建描述符的过程,尤其是在你只需要简单的getter、setter和deleter函数时。

描述符优先级总结

当访问属性时,Python会按照以下优先级顺序查找属性:

  1. 数据描述符 (Data Descriptor)
  2. 实例字典 (__dict__)
  3. 非数据描述符 (Non-Data Descriptor)
  4. 类字典
  5. 父类

总结

描述符协议是Python元编程的重要组成部分, 它允许你自定义属性访问的行为,实现各种高级功能,如数据验证、只读属性和延迟计算。 掌握__get____set____delete__方法的调用顺序以及数据描述符和非数据描述符的区别,能让你更好地理解和利用Python的强大功能。

描述符的核心概念与方法概括

  • 描述符通过__get____set____delete__方法控制属性访问。
  • 数据描述符和非数据描述符在属性查找时具有不同的优先级。
  • property函数是创建描述符的简化方法。

更多IT精英技术系列讲座,到智猿学院

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注