43. 描述符

描述符是一个控制另一个对象的属性访问的对象。描述符是 Python 对象模型背后的主要机制之一。他们解释了方法如何绑定到实例,如何property工作原理,如何staticmethodclassmethod工作原理、槽的工作原理以及有多少 CPython 级别的类型操作连接到 Python 级别的语法。

在语言级别,描述符是定义以下一个或多个方法的任何对象:python id="x7hcrg" __get__(self, obj, objtype=None) __set__(self, obj, value) __delete__(self, obj) 一个对象与__get__是一个描述符。

一个对象与__set__或者__delete__是一个数据描述符。

非数据描述符和数据描述符之间的区别控制着查找优先级。

43.1 为什么描述符存在

Python 属性访问看起来很简单:python id="96l8i5" obj.name 但这个表达式并不意味着“读取一个名为name从记忆中。”

这意味着:```text id="drqc0j" ask the object's type how attribute lookup works search descriptors and dictionaries in a defined order possibly call descriptor methods return the resulting object


示例:```python id="tkau0z"
obj.method
obj.property_name
Class.class_method
Class.static_method
obj.slot_name
```所有这些都涉及描述符行为。

## 43.2 基本描述符协议

描述符定义`__get__````python id="ct8qxy"
class Descriptor:
    def __get__(self, obj, objtype=None):
        return "computed value"
```将其用作类属性:```python id="d0fypg"
class Example:
    value = Descriptor()

e = Example()

print(e.value)
```输出:```text id="qz34l2"
computed value
```描述符对象存储在类上:```python id="csuohy"
print(Example.__dict__["value"])
```但是通过实例调用来访问属性`Descriptor.__get__`

## 43.3 描述符参数

描述符方法接收:

|论证|意义|
|---|---|
|`self`|描述符对象 |
|`obj`|正在访问的实例,或者`None`班级访问|
|`objtype`|业主阶层|

例子:```python id="v4lvjg"
class Descriptor:
    def __get__(self, obj, objtype=None):
        print("obj:", obj)
        print("objtype:", objtype)
        return 42

class Example:
    value = Descriptor()

e = Example()

print(e.value)
print(Example.value)
```例如访问:```text id="by7jq8"
obj: <__main__.Example object at ...>
objtype: <class '__main__.Example'>
```对于类访问:```text id="k4ktgr"
obj: None
objtype: <class '__main__.Example'>
```描述符可以使用此差异为实例和类访问返回不同的值。

## 43.4 非数据描述符

非数据描述符定义`__get__`,但不是`__set__`或者`__delete__````python id="hb1iji"
class NonDataDescriptor:
    def __get__(self, obj, objtype=None):
        return "descriptor value"
```例子:```python id="bvccxu"
class Example:
    value = NonDataDescriptor()

e = Example()

print(e.value)
```输出:```text id="j61t3h"
descriptor value
```但由于它是非数据描述符,实例字典条目可以覆盖它:```python id="vu3t3a"
e.__dict__["value"] = "instance value"

print(e.value)
```输出:```text id="6xj4jn"
instance value
```这种优先顺序是有意为之的。这就是普通方法如何被实例属性隐藏的方式。

## 43.5 数据描述符

数据描述符定义`__set__`或者`__delete__````python id="rk5sgr"
class DataDescriptor:
    def __get__(self, obj, objtype=None):
        return "descriptor value"

    def __set__(self, obj, value):
        print("setting", value)
```例子:```python id="c9rzsd"
class Example:
    value = DataDescriptor()

e = Example()
e.__dict__["value"] = "instance value"

print(e.value)
```输出:```text id="m34bnr"
descriptor value
```数据描述符胜过实例字典。

这条规则对于`property`

## 43.6 查找优先级

对于普通实例属性访问:```python id="ynllax"
obj.name
```CPython的对象查找大致遵循以下顺序:```text id="rpb90z"
1. Look for name on the type or base types.
2. If found and it is a data descriptor, call its __get__.
3. Look in the instance dictionary.
4. If found on the type and it is a non-data descriptor, call its __get__.
5. If found on the type as a normal attribute, return it.
6. If not found, call __getattr__ if defined.
7. Otherwise raise AttributeError.
```此顺序解释了为什么某些类属性可以被实例属性隐藏,而另一些则不能。

## 43.7 描述符优先级示例```python id="n4y91b"
class NonData:
    def __get__(self, obj, objtype=None):
        return "non-data descriptor"

class Data:
    def __get__(self, obj, objtype=None):
        return "data descriptor"

    def __set__(self, obj, value):
        raise AttributeError("read-only")

class Example:
    a = NonData()
    b = Data()

e = Example()
e.__dict__["a"] = "instance a"
e.__dict__["b"] = "instance b"

print(e.a)
print(e.b)
```输出:```text id="0dzdun"
instance a
data descriptor

a被隐藏,因为它是非数据描述符。b没有被隐藏,因为它是一个数据描述符。

43.8 函数是描述符

存储在类上的函数是描述符。```python id="4040yq" class Example: def method(self): return 42

e = Example()

print(Example.dict["method"]) print(e.method)


实例访问返回的对象是绑定方法。```text id="j0sg2u"
function object stored on class
     __get__(instance, class)
bound method object
```这就是为什么:```python id="r1yoxy"
e.method()
```通过`e`作为第一个参数。

## 43.9 方法绑定

函数的描述符行为可以建模如下:```python id="rrmwg4"
class FunctionLike:
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return BoundMethod(self, obj)
```绑定方法存储:```text id="9ac6b1"
function
instance
```调用绑定方法:```python id="zs0r2l"
e.method(1, 2)
```大致相当于:```python id="0jmqjg"
Example.method(e, 1, 2)
```这不是解析器中的特殊语法。它是普通的属性查找加上描述符绑定加上调用。

## 43.10 类对方法的访问

当通过类访问时,函数描述符接收`obj=None````python id="c1o0cb"
class Example:
    def method(self):
        return 42

print(Example.method)
```结果是原始的类似函数的对象,而不是实例的绑定方法。

所以这有效:```python id="ce7spz"
e = Example()

print(Example.method(e))
```该实例是显式传递的。

## 43.11 绑定方法对象

绑定方法对象公开有用的属性:```python id="0hd310"
class Example:
    def method(self):
        return 42

e = Example()
m = e.method

print(m.__self__)
print(m.__func__)

__self__是绑定实例。__func__是底层函数。

从概念上讲:text id="9rhbma" bound_method.__self__ -> e bound_method.__func__ -> Example.__dict__["method"] 呼叫:python id="r7sscl" m() 呼叫:python id="sqt0vq" m.__func__(m.__self__) ## 43.12property

property是一个数据描述符。```python id="o83sqw" class User: def init(self, name): self._name = name

@property
def name(self):
    return self._name

使用权:python id="q15uwm" u = User("Ada") print(u.name)


一个粗略的模型:```python id="e1wqf0"
class property:
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return self.fget(obj)

    def __set__(self, obj, value):
        if self.fset is None:
            raise AttributeError("can't set attribute")
        self.fset(obj, value)
```因为`property`定义`__set__`,它是一个数据描述符。它胜过了实例字典。

## 43.13 只读属性

只读属性仍然充当数据描述符。```python id="bjmyqo"
class Example:
    @property
    def value(self):
        return 42

e = Example()
e.__dict__["value"] = 100

print(e.value)
```输出:```text id="8qw90w"
42
```即使该房产没有设置者,`property`类型有`__set__`引发错误的行为。这使它成为一个数据描述符。

## 43.14 可写属性

可写属性添加一个 setter```python id="7362gp"
class User:
    def __init__(self):
        self._name = ""

    @property
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        if not value:
            raise ValueError("empty name")
        self._name = value
```用法:```python id="ly2z3r"
u = User()
u.name = "Ada"
print(u.name)
```任务:```python id="pzyvzj"
u.name = "Ada"
```调用描述符的`__set__`

它不只是简单地写`u.__dict__["name"]`

## 43.15`staticmethod`

`staticmethod`是抑制方法绑定的描述符。```python id="7h9gwf"
class Math:
    @staticmethod
    def add(a, b):
        return a + b

print(Math.add(2, 3))
print(Math().add(2, 3))
```In both cases, no instance is inserted as the first argument.

一个粗略的模型:```python id="k4n2sv"
class staticmethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        return self.func

staticmethod存储一个函数并原封不动地返回它。

43.16classmethod

classmethod是绑定类而不是实例的描述符。```python id="m0y4q8" class User: @classmethod def create(cls): return cls()

u = User.create() 一个粗略的模型:python id="vn2wvc" class classmethod: def init(self, func): self.func = func

def __get__(self, obj, objtype=None):
    if objtype is None:
        objtype = type(obj)
    return BoundMethod(self.func, objtype)

呼叫:python id="cn0iaw" User.create() 大致相当于:python id="mzdvxl" User.dict["create"].get(None, User)() 其中调用:python id="mgiurn" original_function(User) ```## 43.17__slots__和描述符__slots__使用描述符来管理固定布局实例属性。

例子:```python id="tv0e9k" class Point: slots = ("x", "y")

def __init__(self, x, y):
    self.x = x
    self.y = y

类字典包含槽描述符:python id="ziiwfx" print(Point.dict["x"]) print(Point.dict["y"])


有了槽,普通实例可能没有`__dict__`除非明确要求。```python id="mv07bu"
p = Point(1, 2)

print(hasattr(p, "__dict__"))
```输出:```text id="drcrll"
False
```## 43.18 成员描述符和 Getset 描述符

CPython 将一些 C 级描述符公开为对象。

常见示例:```text id="vtr7cq"
member_descriptor
getset_descriptor
wrapper_descriptor
method_descriptor
```您可以在类词典中看到它们:```python id="7ln1br"
class Point:
    __slots__ = ("x",)

print(type(Point.__dict__["x"]))
```对于内置类型:```python id="inwepf"
print(type(int.__dict__["real"]))
print(type(str.__dict__["upper"]))
print(type(object.__dict__["__str__"]))
```这些是包装 C 级行为的 CPython 级描述符对象。

## 43.19 描述符和属性赋值

对于作业:```python id="4ja4xk"
obj.name = value
```CPython并不总是写入`obj.__dict__`

如果该类型有一个数据描述符`name`,赋值调用描述符的`__set__`

例子:```python id="2t9ndx"
class Descriptor:
    def __set__(self, obj, value):
        print("set", value)

class Example:
    x = Descriptor()

e = Example()
e.x = 10
```输出:```text id="jfd4bi"
set 10
```不需要正常的实例字典写入。

## 43.20 描述符和属性删除

对于删除:```python id="mxpuzk"
del obj.name
```如果数据描述符定义`__delete__`CPython 称之为。```python id="wuc0pw"
class Descriptor:
    def __delete__(self, obj):
        print("delete")

class Example:
    x = Descriptor()

e = Example()
del e.x
```输出:```text id="fkv43g"
delete
```否则,删除可能会从实例字典中删除条目或引发`AttributeError`

## 43.21`__set_name__`描述符可以定义`__set_name__`。```python id="yvbjoc"
class Field:
    def __set_name__(self, owner, name):
        self.owner = owner
        self.name = name

class User:
    id = Field()
    name = Field()
```在类创建期间,Python 调用:```text id="zf52ho"
User.__dict__["id"].__set_name__(User, "id")
User.__dict__["name"].__set_name__(User, "name")
```这可以让描述符了解它们被分配的属性名称。

没有`__set_name__`,描述符通常需要冗余配置:```python id="k9fsdd"
id = Field("id")
name = Field("name")
```## 43.22 实用验证描述符```python id="okxfod"
class PositiveInt:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.name)

    def __set__(self, obj, value):
        if not isinstance(value, int):
            raise TypeError("expected int")
        if value <= 0:
            raise ValueError("expected positive integer")
        setattr(obj, self.name, value)
```用法:```python id="u85219"
class User:
    age = PositiveInt()

    def __init__(self, age):
        self.age = age

u = User(30)
print(u.age)
```作业:```python id="7h7peb"
self.age = age
```来电`PositiveInt.__set__`

访问:```python id="jvsgys"
u.age
```来电`PositiveInt.__get__`

## 43.23 描述符存储选择

描述符通常将每个实例的值存储在其他地方。

常见选择:

|存储|示例|权衡|
|---|---|---|
|实例字典 |`obj.__dict__[name]`|简单,需要`__dict__`|
|私有属性|`obj._name`|简单,可以碰撞|
|弱键字典|`WeakKeyDictionary[obj]`|无需接触实例字典即可工作,开销较高 |
|槽偏移| CPython 内部 |快速、固定布局 |
|外部商店| ORM/会话/状态表 |对框架有用 |

描述符对象通常由类共享,因此将实例特定的数据直接存储在描述符上通常是错误的。

坏的:```python id="httjyo"
class BadField:
    def __set__(self, obj, value):
        self.value = value
```所有实例共享一个描述符对象。

更好的:```python id="qgjw4l"
class Field:
    def __set_name__(self, owner, name):
        self.name = "_" + name

    def __set__(self, obj, value):
        setattr(obj, self.name, value)
```## 43.24 描述符共享

描述符存储在类上。```python id="poo2z7"
class Field:
    pass

class User:
    name = Field()
```有一个`Field`对象为`User.name`

全部`User`实例访问相同的描述符对象。```python id="ghzk97"
print(User.__dict__["name"])
```这就是为什么必须仔细设计描述符状态的原因。

描述符级状态有利于:```text id="lzdzy9"
field name
validation rule
default configuration
metadata
owner class
```实例级状态属于实例或外部每个实例存储。

## 43.25 描述符和继承

描述符像其他类属性一样被继承。```python id="07zg83"
class Field:
    def __get__(self, obj, objtype=None):
        return "value"

class Base:
    x = Field()

class Child(Base):
    pass

print(Child().x)
```输出:```text id="23j8kz"
value
```Lookup 搜索方法解析顺序。如果在基类中找到描述符,则它参与子实例的属性访问。

子类可以通过定义相同的名称来覆盖描述符。```python id="jgv5bi"
class Child(Base):
    x = 100
```现在:```python id="a343w1"
print(Child().x)
```返回:```text id="sbrd9y"
100
```除非其他查找规则适用。

## 43.26 描述符和`super`

`super()`还使用描述符绑定。

例子:```python id="klco94"
class Base:
    def method(self):
        return "base"

class Child(Base):
    def method(self):
        return super().method()
```表达式:```python id="vz6j6v"
super().method
```发现`method`MRO 中的下一个类并将其绑定到原始实例。

该绑定仍然使用描述符逻辑。

## 43.27 描述符和`__getattribute__`所有正常的属性访问都会经过`__getattribute__`。```python id="0h0zfk"
obj.name
```大致调用:```python id="wrm9c8"
type(obj).__getattribute__(obj, "name")
```默认实现,`object.__getattribute__`,实现描述符查找。

如果一个类重写了`__getattribute__`,它可以更改或绕过描述符行为。

例子:```python id="6hlojz"
class Example:
    @property
    def x(self):
        return 42

    def __getattribute__(self, name):
        return "intercepted"

e = Example()
print(e.x)
```输出:```text id="i8f18x"
intercepted
```该属性从未到达,因为`__getattribute__`拦截一切。

## 43.28 描述符和`__getattr__`

`__getattr__`仅在正常查找失败后调用。```python id="1tg4g8"
class Example:
    def __getattr__(self, name):
        return "missing"

e = Example()
print(e.anything)
```输出:```text id="cafj83"
missing
```如果描述符存在并返回一个值,`__getattr__`不被调用。

查找顺序:```text id="hpfu2m"
__getattribute__ runs first
descriptor rules happen inside normal __getattribute__
__getattr__ handles missing names only
```## 43.29 描述符和元类

类属性访问也使用描述符逻辑,但正在搜索的对象是类对象。```python id="6g2hvo"
class Meta(type):
    @property
    def label(cls):
        return cls.__name__.lower()

class User(metaclass=Meta):
    pass

print(User.label)
```这里,`label`是元类的描述符。访问`User.label`调用描述符查找`type(User)`

这就是元类可以定义计算类属性的原因。

## 43.30 类的描述符查找

对于:```python id="bugwo3"
Class.attr
```查找由处理`type.__getattribute__`

搜索发生在类对象及其元类上。

如果元类中的属性是描述符,它可以与作为对象的类绑定。

就是这样`classmethod`、元类属性和许多内置类型操作都可以工作。

类查找和实例查找相关但不相同。实例查找搜索实例类型的 MRO。类查找通过元类机制进行搜索。

## 43.31 ORM 中的描述符

描述符在 ORM 中很常见。

形状示例:```python id="gs116j"
class Column:
    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return obj._row[self.name]

    def __set__(self, obj, value):
        obj._row[self.name] = value
```用法:```python id="3gj06d"
class User:
    id = Column()
    name = Column()

    def __init__(self, row):
        self._row = row
```然后:```python id="e6nz6o"
u = User({"id": 1, "name": "Ada"})
print(u.name)
u.name = "Grace"
```描述符将属性访问映射到行存储。

## 43.32 验证库中的描述符

验证框架使用描述符来强制执行约束。```python id="kcyh88"
class String:
    def __set_name__(self, owner, name):
        self.storage = "_" + name

    def __set__(self, obj, value):
        if not isinstance(value, str):
            raise TypeError("expected str")
        setattr(obj, self.storage, value)

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        return getattr(obj, self.storage)
```用法:```python id="4vl57c"
class User:
    name = String()

    def __init__(self, name):
        self.name = name
```类定义声明了该字段。描述符强制执行该规则。

## 43.33 缓存计算中的描述符

缓存的属性是非数据描述符。```python id="yh220g"
class cached_property:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self

        value = self.func(obj)
        obj.__dict__[self.name] = value
        return value
```用法:```python id="kxogrn"
class Data:
    @cached_property
    def total(self):
        print("computing")
        return 100

d = Data()
print(d.total)
print(d.total)
```第一次访问调用描述符并将值存储在实例字典中。

第二次访问返回实例字典值,因为描述符是非数据。

这直接依赖于描述符优先级。

## 43.34 数据与非数据设计规则

当描述符必须控制写入或不得被实例属性覆盖时,请使用数据描述符。

当需要实例级缓存或阴影时,请使用非数据描述符。

|描述符类型 |定义|实例字典可以覆盖吗? |常见示例|
|---|---|---:|---|
|非数据描述符 |`__get__`|是的 |函数、缓存属性 |
|数据描述符|`__set__`或者`__delete__`|没有 |属性、槽、验证字段 |

这一区别解释了许多属性访问行为。

## 43.35 CPython 描述符的内部槽

 C 级别,描述符行为通过类型槽表示。

相关操作对应:```text id="3v2e6f"
tp_descr_get
tp_descr_set
```实现的类型`tp_descr_get`可以像以前一样`__get__`

实现的类型`tp_descr_set`可以像以前一样`__set__`或者`__delete__`

内置描述符通常是 C 对象,其类型提供这些槽。

这就是内置方法、槽描述符、getset 描述符和包装器描述符与普通 Python 查找集成的方式。

## 43.36 CPython 术语中的属性查找

简化的 CPython 级查找`obj.name`:

```text id="m04p6t"
1. type = Py_TYPE(obj)
2. descr = lookup name in type MRO
3. if descr has tp_descr_get and tp_descr_set:
       return descr.__get__(obj, type)
4. if obj has dict and name in dict:
       return dict[name]
5. if descr has tp_descr_get:
       return descr.__get__(obj, type)
6. if descr exists:
       return descr
7. call fallback or raise AttributeError
```这是描述符的操作核心。

## 43.37 描述符错误

描述符应该提高`AttributeError`当他们想要正常的属性回退行为时缺少属性。

例子:```python id="xeje8w"
class Maybe:
    def __get__(self, obj, objtype=None):
        raise AttributeError("not available")
```引发的描述符`AttributeError`可能会与`getattr`, `hasattr`和后备机制。

提出精确的异常。不要隐藏不相关的错误`AttributeError`除非该属性确实不可用。

## 43.38 描述符内省

要检查描述符,请通过类字典访问它。```python id="047r6b"
class Example:
    @property
    def value(self):
        return 42

print(Example.__dict__["value"])
```通过类访问可能会调用`__get__`:

```python id="2rtf24"
print(Example.value)
```为了`property`,类访问返回属性对象。但其他描述符可能会返回计算值。

检索原始描述符的最安全方法通常是:```python id="gvvuc9"
vars(Example)["value"]
```或者:```python id="h6m1qm"
Example.__dict__["value"]
```## 43.39 常见描述符错误

|错误 |原因 |修复 |
|---|---|---|
|所有实例共享一个值 |在描述符上存储实例状态 |将状态存储在实例或外部每个实例映射上 |
|自定义类忽略的属性 |超越`__getattribute__`错误|委托给`object.__getattribute__`|
|缓存属性不缓存 |描述符是数据描述符|使其成为非数据或编写自定义优先级逻辑 |
|实例属性不能覆盖字段|描述符定义`__set__`|如果需要覆盖,请使用非数据描述符 |
|描述符缺少字段名称 |没有实施`__set_name__`|添加`__set_name__`或显式传递名称 |
|类访问中断 |`__get__`不处理`obj is None`|返回描述符或类级对象 |

## 43.40 要点

描述符是定义的对象`__get__`, `__set__` 或者`__delete__`

非数据描述符定义`__get__`仅有的。

数据描述符定义`__set__`或者`__delete__`

数据描述符优先于实例字典。

非数据描述符可以被实例字典隐藏。

函数是描述符,这就是方法绑定到实例的原因。`property`, `staticmethod`, `classmethod`、槽、内置方法和许多 CPython 内部都使用描述符。

描述符在 CPython 中是通过类型对象上的描述符槽实现的。

了解描述符是理解方法、属性、槽、元类和属性查找所必需的。