>>> from env_helper import info; info()
页面更新时间: 2024-03-29 16:20:49
运行环境:
    Linux发行版本: Debian GNU/Linux 12 (bookworm)
    操作系统内核: Linux-6.1.0-18-amd64-x86_64-with-glibc2.36
    Python版本: 3.11.2

1.20. 属性描述符

学会描述符之后,不仅有更多的工具集可用,还会对 Python 的运作方式有更深入的理解,并由衷赞叹 Python 设计的优雅。
——Raymond Hettinger, Python 核心开发者和专家
本章的话题是描述符。
描述符是实现了特定协议的类,这个协议包括 __get____set__、和 __delete__ 方法。

有了它,我们就可以在类上定义一个托管属性,并把所有对实例中托管属性的读写操作交给描述符类去处理。

>>> # 描述符示例:将一个属性托管给一个描述符类
>>> class CharField:                       # 描述符类
>>>     def __init__(self, field_name):
>>>         self.field_name = field_name
>>>
>>>     def __get__(self, instance, storage_cls):
>>>         print('__get__', instance, storage_cls)
>>>         if instance is None:            # 直接在类上访问托管属性时,instance 为 None
>>>             return self
>>>         return instance[self.field_name]
>>>
>>>     def __set__(self, instance, value):
>>>         print('__set__', instance, value)
>>>         if not isinstance(value, str):
>>>             raise TypeError('Value should be string')
>>>         instance[self.field_name] = value
>>>
>>>
>>> class SomeModel:                         # 托管类
>>>     name = CharField('name')             # 描述符实例,也是托管类中的托管属性
>>>
>>>     def __init__(self, **kwargs):
>>>         self._dict = kwargs              # 出巡属性,用于存储属性
>>>
>>>     def __getitem__(self, item):
>>>         return self._dict[item]
>>>
>>>     def __setitem__(self, item, value):
>>>         self._dict[item] = value
>>>
>>>
>>>
>>> print(SomeModel.name)
>>> d = SomeModel(name='some name')
>>> print(d.name)
>>> d.name = 'another name'
>>> print(d.name)
>>> try:
>>>     d.name = 1
>>> except Exception as e:
>>>     print(repr(e))
__get__ None <class '__main__.SomeModel'>
<__main__.CharField object at 0x063AF1F0>
__get__ <__main__.SomeModel object at 0x063AF4B0> <class '__main__.SomeModel'>
some name
__set__ <__main__.SomeModel object at 0x063AF4B0> another name
__get__ <__main__.SomeModel object at 0x063AF4B0> <class '__main__.SomeModel'>
another name
__set__ <__main__.SomeModel object at 0x063AF4B0> 1
TypeError('Value should be string')

1.20.1. 描述符的种类

根据描述符上实现的方法类型,我们可以把描述符分为覆盖型描述符非覆盖型描述符

实现 __set__ 方法的描述符属于覆盖型描述符,因为虽然描述符是类属性,但是实现 __set__ 方法的话,会覆盖对实例属性的赋值操作。
而没有实现 __set__ 方法的描述符是非覆盖型描述符。对实例的托管属性赋值,则会覆盖掉原有的描述符属性,此后再访问该属性时,将不会触发描述符的 __get__ 操作。如果想恢复原有的描述符行为,则需要用 del 把覆盖掉的属性删除。

具体可以看官方 Repo 的例子

1.20.2. 描述符的用法建议

  • 如果只想实现一个只读描述符,可以考虑使用 property 而不是自己去实现描述符;

  • 只读描述符必须有 __set__ 方法,在方法内抛出 AttributeError,防止属性在写时被覆盖;

  • 用于验证的描述符可以只有 __set__ 方法:通过验证后,可以修改 self.__dict__[key] 来将属性写入对象;

  • 仅有 __get__ 方法的描述符可以实现高效缓存;

  • 非特殊的方法可以被实例属性覆盖。