好的,下面我们深入探讨Python中的描述符协议。
Python描述符协议:理解__get__、__set__、__delete__的调用顺序
今天我们要深入探讨Python中一个非常强大但有时令人困惑的特性:描述符协议(Descriptor Protocol)。 掌握描述符是成为Python高级程序员的关键一步,它允许你控制属性访问,实现数据验证,创建只读属性,等等。 我们将详细了解__get__、__set__和__delete__方法,以及它们在不同场景下的调用顺序。
什么是描述符?
简单来说,描述符是一个实现了描述符协议的Python对象。描述符协议定义了__get__、__set__和__delete__这三个特殊方法。当对象的属性被访问、设置或删除时,如果该属性是一个描述符,那么这些特殊方法就会被调用。
描述符协议方法
-
__get__(self, instance, owner): 访问属性时调用。self: 描述符实例本身。instance: 拥有者类的实例。如果通过类访问描述符,则instance为None。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参数为None, owner参数为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继承了ParentClass的my_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覆盖了ParentClass的my_attribute。 ChildDescriptor可以有自己的逻辑,并且可以选择调用父类的__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会按照以下优先级顺序查找属性:
- 数据描述符 (Data Descriptor)
- 实例字典 (
__dict__) - 非数据描述符 (Non-Data Descriptor)
- 类字典
- 父类
总结
描述符协议是Python元编程的重要组成部分, 它允许你自定义属性访问的行为,实现各种高级功能,如数据验证、只读属性和延迟计算。 掌握__get__、__set__和__delete__方法的调用顺序以及数据描述符和非数据描述符的区别,能让你更好地理解和利用Python的强大功能。
描述符的核心概念与方法概括
- 描述符通过
__get__、__set__、__delete__方法控制属性访问。 - 数据描述符和非数据描述符在属性查找时具有不同的优先级。
property函数是创建描述符的简化方法。
更多IT精英技术系列讲座,到智猿学院