Python 描述符协议(descriptor protocol)
参考:
- Clean Code in Python Refactor your legacy codebase by Mariano Anaya (z-lib.org), Chapter 6
- Python document: Descriptor HowTo Guide
- Python implementing descriptors
描述符协议
Simple example
#!/usr/bin/env python
# -*- coding: utf-8 -*-
class DescriptorClass(object):
value = 42
def __get__(self, instance, owner):
if instance is None:
return f"{self.__class__.__name__}.{owner.__name__}"
return f"value for {instance}"
class ClientClass(object):
descriptor = DescriptorClass()
def main():
client = ClientClass()
# output: DescriptorClass.ClientClass
print(ClientClass.descriptor)
# output: value for <__main__.ClientClass object at 0x...>
print(client.descriptor)
if __name__ == '__main__':
main()
__get__(self, instance, owner)
当我们希望获取描述符的值时, 该方法会被调用. 方法参数解释如下:
self : 表示 descriptor 对象本身
instance : 当 descriptor 被实例调用时, 表示该实例, 如果是被类调用, 则该参数为 None
owner : 表示 instance 参数的所属的类
因为 descriptor 是作为类的属性存在的, 所以是可以通过 ClientClass.descriptor 这种方式调用的. __get__ 的接口会看起来显得如此奇怪的原因就是为了保证不管是通过实例调用(obj.descriptor)还是通过类调用(Class.descriptor) , 该接口都能正常工作.
通常来说, 除非我们希望对 owner 进行一些操作, 否则当 instance 为 None 时, 一般返回描述符本身比较好.
__set__(self, instance, value)
当我们对描述符进行赋值操作时, 该方法会被调用. self 参数和 instance 参数同前, value 参数则表示被赋值的具体值.
当我们通过 client.descriptor = "value"
这样的调用方式对描述符进行赋值时, 如果描述符定义了 __set__ 方法,则会调用描述符的 __set__ 方法进行赋值, 此时 value 参数被设为 “value” . 如果描述符没有定义 __set__ 方法,那么 client.descriptor 会被直接覆盖为字符串 “value”. 因此在对描述符进行赋值时,确保该描述符实现了 __set__ 方法.
__delete__(self, instance)
当调用 del 语句从实例中删除描述符时, 该方法会被调用. 例如 del client.descriptor
.
__set_name__(self, owner, name)
当我们在一个Class中使用描述符时,我们需要在实例化该描述符时传入该属性的名字. Python 3.6 之前, 这个名字都是通过初始化的时候手动传入的. 如下所示:
class DescriptorWithName:
def __init__(self, name):
self.name = name
def __get__(self, instance, value):
if instance is None:
return self
print("getting %r attribute from %r", self.name, instance)
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class ClientClass:
descriptor = DescriptorWithName("descriptor")
Python 3.6 之后, 添加了 __set_name__ 方法, 该方法在描述符被创建时会被调用,因此我们可以不用在创建描述符时输入两次描述符的名字. 如下:
class DescriptorWithName:
def __init__(self, name=None):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
print("getting %r attribute from %r", self.name, instance)
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class ClientClass2:
descriptor = DescriptorWithName()
def __init__(self, descriptor):
self.descriptor = descriptor
注意 : 为了和3.6之前的保持兼容, DescriptorWithName 类的 init 函数仍旧保留了 name 参数, 但是为其赋予了默认值.
描述符的分类
Python的描述符可以分为两类.
- non-data descriptor 仅实现了 __get__ 方法的描述符
- data-descriptor 实现了 __set__ 和 __delete__ 方法的描述符
__set_name__ 方法的实现与否, 不影响描述符的类型. 下面分别介绍两种描述符类型.
non-data descriptor
class NonDataDescriptor(object):
def __get__(self, instance, owner):
if instance is None:
return self
return 42
class ClientClass:
descriptor = NonDataDescriptor()
def main():
client = ClientClass()
print(client.__dict__)
print(client.descriptor, type(client.descriptor))
client.descriptor = 43
print(client.__dict__)
print(client.descriptor, type(client.descriptor))
del client.descriptor
print(client.__dict__)
print(client.descriptor, type(client.descriptor))
# output
# {}
# 42 <class 'int'>
# {'descriptor': 43}
# 43 <class 'int'>
# {}
# 42 <class 'int'>
if __name__ == '__main__':
main()
下面详细讲解一下整个过程.
当我们创建 client 对象时, descriptor 存在于 ClientClass 类中, 而不是在 client 对象中. 当我们通过 client.descriptor 方式访问 descriptor 时, 解释器首先在 client.__dict__ 字典中查找 descriptor 属性, 查找不到后会转到 client 对象的类的, 也就是 ClientClass __dict__ 字典中查找. 查找成功,但是因为 NonDataDescriptor 的 __get__ 方法只是返回一个固定值, 并不会修改 instance 对象的 __dict__ 字典, 因此即使我们通过 client.descriptor 方式访问过描述符的值, 但是 client 对象的 __dict__ 字典仍然是空的.
一旦通过 client.descriptor = 43 语句向 client 对象赋予了 descriptor 后, 其 __dict__ 字典被更新了, 因此当我们再次通过 client.descriptor 时, 解释器可以在 client.__dict__ 中查找到 descriptor, 直接就返回了, 而不会再进入 NonDataDescriptor.__get__ 方法.
手动删除 del client.descriptor client.descriptor 属性后, client 的 __dict__ 字典又变为空, 因此再次访问 client.descriptor 时, 又和最初一样, 进入到 NonDataDescriptor.__get__ 中, 返回值为 42.
理解这个过程的关键在于清楚 Python 解释器查找实例属性的过程: 简单来说就是就近原则. 先在实例的 __dict__ 字典中查找, 如果查找失败, 就在实例所属的类的 __dict__ 字典中查(参考 Python Data Model: The standard type hierarchy, Class instances 一节).
data descriptor
class DataDescriptor:
def __get__(self, instance, owner):
if instance is None:
return self
return 42
def __set__(self, instance, value):
print("setting {}.descriptor to {}".format(instance, value))
instance.__dict__["descriptor"] = value
class ClientClass:
descriptor = DataDescriptor()
def main():
client = ClientClass()
print(ClientClass.__dict__)
print(client.__dict__)
print(client.descriptor, type(client.descriptor))
client.descriptor = 43
print(client.__dict__)
print(client.descriptor, type(client.descriptor))
print(client.__dict__)
print(client.descriptor, type(client.descriptor))
# output
# {}
# 42 <class 'int'>
# setting <__main__.ClientClass object at 0x7f520690ed30>.descriptor to 43
# {'descriptor': 43}
# 42 <class 'int'>
# {'descriptor': 43}
# 42 <class 'int'>
if __name__ == '__main__':
main()
注意 : 对 data-decriptor 调用 del 方法时, 解释器不会去删除 __dict__ 中同名的属性, 而是会调用描述符的 __delete__ 方法. 当描述符没有定义该方法时, 解释器会抛出 AttributeError .