43. 描述符
43. 描述符
描述符是一个控制另一个对象的属性访问的对象。描述符是 Python 对象模型背后的主要机制之一。他们解释了方法如何绑定到实例,如何property工作原理,如何staticmethod和classmethod工作原理、槽的工作原理以及有多少 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 中是通过类型对象上的描述符槽实现的。
了解描述符是理解方法、属性、槽、元类和属性查找所必需的。