33. 属性查找
33. 属性查找
属性查找是用于计算表达式的运行时过程,例如:```python id="5ob0m9" obj.name
一个简单的属性表达式可以触发大量的机制:```text id="b2zmcv"
find the object's type
search the type and its base classes
handle descriptors
check the instance dictionary
call custom attribute hooks
return a value or raise AttributeError
```属性查找是动态的。结果可能取决于运行时对象、其类、其基类、其实例字典、描述符、元类和用户定义的挂钩。
## 33.1 基本属性访问
表达式:```python id="67bubm"
obj.x
```要求 CPython 查找名为的属性`"x"`在`obj`。
如果找到,则查找返回一个 Python 对象。
如果缺失,则会引发`AttributeError`。
例子:```python id="l53g0p"
class C:
pass
obj = C()
obj.x = 10
print(obj.x)
```作业商店`x`在实例字典中:```text id="kz7fxs"
obj.__dict__["x"] = 10
```访问权限在那里找到它。
## 33.2 属性查找不仅仅是字典查找
对于简单对象,属性访问可能看起来像字典查找:```python id="m2qolp"
obj.__dict__["x"]
```但完整属性查找更为复杂。
这个表达式:```python id="1v72yi"
obj.x
```可能涉及:```text id="h2b2fw"
data descriptors
instance dictionary
non-data descriptors
class attributes
base classes
__getattribute__
__getattr__
slots
properties
metaclass behavior
```所以这个等价是不完整的:```python id="ue4afg"
obj.x == obj.__dict__["x"]
```它只适用于简单的情况。
## 33.3 实例字典
大多数普通的 Python 对象都有一个实例字典。```python id="m08lyl"
class User:
pass
u = User()
u.name = "Ada"
u.age = 37
print(u.__dict__)
```输出:```text id="7fbd65"
{'name': 'Ada', 'age': 37}
```除非类使用槽或自定义布局,否则实例属性存储在该字典中。
为了:```python id="v04kyu"
u.name
```CPython可以找到`"name"`在`u.__dict__`。
但实例字典并不总是首先检查。类上的数据描述符具有优先权。
## 33.4 类属性
属性可以存在于类中。```python id="f16lwf"
class User:
kind = "human"
u = User()
print(u.kind)
```这里,`kind`不在`u.__dict__`。它是在`User.__dict__`。
从概念上讲:```text id="lpc7u3"
u.__dict__ does not contain "kind"
User.__dict__ contains "kind"
return "human"
```类属性通过类共享:```python id="qoumbj"
a = User()
b = User()
print(a.kind)
print(b.kind)
```两个实例都看到相同的类属性,除非一个实例遮蔽了它。
## 33.5 实例属性遮蔽
当类属性不是数据描述符时,实例属性可以隐藏类属性。```python id="g0f5q9"
class User:
kind = "human"
u = User()
u.kind = "admin"
print(u.kind)
print(User.kind)
```输出:```text id="la6r0l"
admin
human
```现在:```text id="hoyjwm"
u.__dict__["kind"] = "admin"
User.__dict__["kind"] = "human"
```实例属性获胜`u.kind`。
这就是为什么类属性应该谨慎用于可变值:```python id="bfuqvu"
class Bag:
items = []
a = Bag()
b = Bag()
a.items.append("x")
print(b.items)
```两个实例看到相同的列表,除非`items`在实例上被覆盖。
## 33.6 属性查找顺序
对于正常的实例属性访问,查找顺序大致为:```text id="vtge5s"
1. Call type(obj).__getattribute__(obj, name)
2. Search the class and base classes for name
3. If a data descriptor is found, call descriptor.__get__
4. Otherwise, check obj.__dict__
5. If found in obj.__dict__, return that value
6. Otherwise, if a non-data descriptor was found, call descriptor.__get__
7. Otherwise, if a class attribute was found, return it
8. Otherwise, call __getattr__ if defined
9. Otherwise, raise AttributeError
```最重要的优先规则是:```text id="rkgfxz"
data descriptor
before instance dictionary
instance dictionary
before non-data descriptor
non-data descriptor or class attribute
after instance dictionary
```此顺序解释了属性、方法、槽和阴影。
## 33.7 描述符
描述符是一个通过一个或多个特殊方法控制属性访问的对象:```text id="hh6upi"
__get__
__set__
__delete__
```描述符存储在类中。
例子:```python id="wgub37"
class Descriptor:
def __get__(self, obj, cls):
return "computed"
class C:
x = Descriptor()
obj = C()
print(obj.x)
```访问`obj.x`呼叫:```python id="tbvfo8"
C.__dict__["x"].__get__(obj, C)
```描述符允许类自定义属性访问的含义。
他们的力量:```text id="z7v9od"
methods
staticmethod
classmethod
property
slots
many built-in attributes
ORM fields
validation systems
lazy attributes
```## 33.8 数据描述符
数据描述符定义`__set__`或者`__delete__`,通常与`__get__`。```python id="lyqqpq"
class DataDescriptor:
def __get__(self, obj, cls):
return "from descriptor"
def __set__(self, obj, value):
print("set", value)
class C:
x = DataDescriptor()
obj = C()
obj.__dict__["x"] = "from dict"
print(obj.x)
```输出:```text id="gvfbh3"
from descriptor
```描述符胜过实例字典,因为它是数据描述符。
就是这样`property`作品。```python id="aqv8hu"
class C:
@property
def x(self):
return 10
obj = C()
print(obj.x)
```属性对象是一个数据描述符。
## 33.9 非数据描述符
非数据描述符定义`__get__`但不是`__set__`或者`__delete__`。
存储在类上的普通 Python 函数是非数据描述符。```python id="sz0xpl"
class C:
def f(self):
return "method"
obj = C()
print(obj.f())
```函数描述符绑定`obj`作为`self`。
但由于它是非数据,实例属性可以隐藏它:```python id="o5g3nx"
obj.f = lambda: "instance function"
print(obj.f())
```现在实例字典获胜。
这就是为什么每个实例都可以替换方法的原因。
## 33.10 属性
属性是在属性访问期间调用函数的描述符。```python id="78465u"
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
return 3.14159 * self.radius * self.radius
c = Circle(10)
print(c.area)
```表达式:```python id="vxxcx9"
c.area
```调用属性获取器。
There is no explicit`()`在源代码中,因为调用隐藏在描述符访问内部。
setter 添加赋值行为:```python id="b245cc"
class User:
def __init__(self):
self._name = ""
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value.strip()
```任务:```python id="acg7af"
u.name = " Ada "
```调用属性设置器。
## 33.11 作为描述符的方法
存储在类上的函数是描述符。```python id="3l3xgm"
class C:
def f(self):
return 1
obj = C()
m = obj.f
```查找:```python id="fp1ymr"
obj.f
```调用函数对象的描述符逻辑并生成绑定方法。
从概念上讲:```text id="1e8hzy"
C.__dict__["f"].__get__(obj, C)
-> bound method
```绑定方法存储:```text id="257g0u"
function = C.f
self = obj
```然后:```python id="jh0x78"
obj.f()
```调用该函数`self`自动插入。
## 33.12`staticmethod`和`classmethod`
`staticmethod`和`classmethod`是描述符包装器。
静态方法禁用绑定:```python id="hmwv5c"
class Math:
@staticmethod
def add(a, b):
return a + b
Math.add(1, 2)
Math().add(1, 2)
```不`self`或者`cls`已插入。
类方法绑定类:```python id="0p2qg1"
class User:
@classmethod
def make(cls):
return cls()
```呼叫:```python id="jpqkoq"
User.make()
```通过`User`作为第一个参数。
通过实例调用也会传递类:```python id="x77e2w"
User().make()
```## 33.13 老虎机`__slots__`更改实例属性存储。```python id="q0q88q"
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
```实例`Point`通常没有正常的`__dict__`除非明确要求。```python id="jmosbt"
p = Point(1, 2)
print(p.x)
```槽属性是通过存储在类中的描述符来实现的。
从概念上讲:```text id="cq58r6"
Point.__dict__["x"] = slot descriptor
Point.__dict__["y"] = slot descriptor
```访问`p.x`调用槽描述符,该描述符从对象布局中的固定位置读取。
槽可以减少内存使用并加速某些属性访问模式,但它们也限制动态实例属性。
## 33.14 缺少属性
如果正常查找失败,Python可能会调用`__getattr__`。```python id="r0ezoa"
class Dynamic:
def __getattr__(self, name):
if name == "answer":
return 42
raise AttributeError(name)
d = Dynamic()
print(d.answer)
__getattr__仅在正常查找路径失败后调用。
这对于:text id="16oz5d" lazy loading proxy objects RPC clients compatibility layers dynamic APIs mock objects 如果__getattr__也失败了,它应该提高AttributeError。
33.15__getattribute__
__getattribute__拦截所有正常的属性访问。```python id="k4hcdi"
class Traced:
def getattribute(self, name):
print("lookup", name)
return super().getattribute(name)
def f(self):
return 1
obj = Traced()
obj.f()
```每次访问都会经过__getattribute__,包括方法查找。
自定义实现必须避免无限递归:python id="6fn49h" class Bad: def __getattribute__(self, name): return self.__dict__[name] 访问self.__dict__来电__getattribute__再次。
使用object.__getattribute__或者super().__getattribute__:
class Good:
def __getattribute__(self, name):
return object.__getattribute__(self, name)
```## 33.16 属性赋值
作业:```python id="in3koz"
obj.x = value
```使用属性设置逻辑。
粗略的顺序是:```text id="mya0tv"
1. Call type(obj).__setattr__(obj, name, value)
2. If a data descriptor with __set__ exists, use it
3. Otherwise, store into obj.__dict__ if available
4. Otherwise, use slot storage if applicable
5. Otherwise, raise AttributeError
```定制`__setattr__`可以拦截赋值:```python id="x89t2s"
class C:
def __setattr__(self, name, value):
print("set", name, value)
super().__setattr__(name, value)
obj = C()
obj.x = 10
```与`__getattribute__`,粗心的代码可能会递归。
## 33.17 属性删除
删除:```python id="annac7"
del obj.x
```使用删除逻辑。
它可能会调用:```text id="wvs69o"
type(obj).__delattr__
descriptor.__delete__
remove from instance dictionary
clear slot value
```描述符示例:```python id="05vatk"
class D:
def __delete__(self, obj):
print("delete")
class C:
x = D()
obj = C()
del obj.x
```删除是同一描述符和属性协议族的一部分。
## 33.18 类属性查找
类也是对象。```python id="n3g515"
class C:
x = 10
print(C.x)
```抬头看`C.x`是对类对象的属性访问。
的类型`C`通常是`type`,因此类属性查找由元类行为控制。
从概念上讲:```text id="qro2hh"
object = C
type(object) = type
lookup attribute "x"
```这就是元类可以自定义类级别属性访问的原因。
## 33.19 元类属性查找
元类可以定义类上可见的属性。```python id="2pzc6p"
class Meta(type):
def meta_method(cls):
return "meta"
class C(metaclass=Meta):
pass
print(C.meta_method())
```该方法在元类上找到并绑定到类对象。
类属性查找涉及:```text id="3rcosr"
attributes on the class itself
descriptors in the metaclass
metaclass MRO
```元类查找与实例查找是分开的,但它使用相同的通用对象模型。
## 33.20 模块属性查找
模块有属性字典。```python id="16mk8q"
import math
print(math.pi)
```这大致是:```text id="iw6moh"
math.__dict__["pi"]
```模块还可以定义`__getattr__`对于缺少的属性。```python id="a4cpwz"
# module.py
def __getattr__(name):
if name == "lazy":
return load_lazy()
raise AttributeError(name)
```然后:```python id="pme0mb"
module.lazy
```可以动态计算。
此功能通常用于延迟导入和兼容性垫片。
## 33.21 属性查找和继承
例如,类查找遵循方法解析顺序。```python id="nri7kz"
class A:
x = "A"
class B(A):
pass
obj = B()
print(obj.x)
```查找搜索:```text id="3gw1ky"
B
A
object
```订单存储在:```python id="il7bpl"
print(B.__mro__)
```多重继承使用 C3 线性化来计算一致的 MRO。```python id="iob9c0"
class A: pass
class B: pass
class C(A, B): pass
print(C.__mro__)
```属性查找依赖于这个顺序。
## 33.22 属性查找和`super`
`super()`更改查找开始的位置。```python id="plcgfy"
class Base:
def f(self):
return "base"
class Child(Base):
def f(self):
return super().f()
```里面`Child.f`, `super().f`搜索后`Child`在 MRO 中,然后将找到的方法绑定到原始实例。
从概念上讲:```text id="zj3hcg"
MRO: Child, Base, object
super from Child
search Base, then object
bind result to self
super()不是简单的父类访问。它是 MRO 相关描述符查找。
33.23 特殊方法查找
通常在类型上查找特殊方法,而不是通过正常的实例属性查找。
例子:python id="2r7t8j" len(obj) 使用类型的长度槽。它不只是执行:python id="5oiizr" obj.__len__() 这种区别很重要:```python id="76o4mf"
class C:
def len(self):
return 10
obj = C() obj.len = lambda: 20
print(obj.len())
print(len(obj))
```显式调用可以找到实例属性。这len()操作使用特殊方法通过类型查找。
特殊方法查找是为了对象协议的速度和一致性而设计的。
33.24 字节码中的属性查找
属性访问编译为字节码指令,例如:text id="wifw5q" LOAD_ATTR STORE_ATTR DELETE_ATTR LOAD_METHOD 为了:python id="it1gq1" value = obj.x 概念字节码:text id="gp7253" LOAD_FAST obj LOAD_ATTR x STORE_FAST value 为了:python id="6xdbsd" obj.x = value 概念字节码:text id="id6s81" LOAD_FAST value LOAD_FAST obj STORE_ATTR x 对于方法调用:```python id="qou4id"
obj.f(arg)
## 33.25 属性查找可以执行代码
属性查找并不总是被动的。
这个表达式:```python id="i49h4b"
obj.x
```可以通过以下方式调用用户代码:```text id="tqt8ka"
__getattribute__
descriptor __get__
property getter
__getattr__
metaclass hooks
module __getattr__
```因此属性访问可以:```text id="gq9mxu"
raise exceptions
mutate state
perform I/O
allocate objects
return different values each time
call arbitrary Python code
```例子:```python id="lqpgto"
class C:
@property
def x(self):
print("computed")
return 10
obj = C()
obj.x
obj.x
```getter 每次都会运行。
## 33.26 属性查找和异常
如果缺少某个属性,Python 会引发`AttributeError`。```python id="u5hs78"
obj.missing
```但属性查找也可能引发其他异常。
例子:```python id="yxnfz8"
class C:
@property
def x(self):
raise RuntimeError("failed")
obj = C()
obj.x
```这引发了`RuntimeError`, 不是`AttributeError`。
通常只有缺少的属性才会引发`AttributeError`。工具如`hasattr`取决于这个约定。```python id="glsscx"
hasattr(obj, "x")
```通过尝试查找和捕获来工作`AttributeError`。
## 33.27 属性查找和`hasattr`
`hasattr(obj, name)`调用属性查找。```python id="sf1vip"
hasattr(obj, "x")
```大致是:```python id="d7n5ls"
try:
getattr(obj, "x")
except AttributeError:
return False
else:
return True
```这意味着`hasattr`可以执行用户代码。
如果一个property getter加注`RuntimeError`, `hasattr`不将其视为缺失属性。```python id="6ci9p0"
class C:
@property
def x(self):
raise RuntimeError("boom")
hasattr(C(), "x")
```这`RuntimeError`传播。
## 33.28 属性查找和`getattr`
`getattr`执行动态属性查找。```python id="uk5dyw"
getattr(obj, "name")
```相当于:```python id="qatskj"
obj.name
```当属性名称静态已知时。
它还支持默认值:```python id="l96rs7"
getattr(obj, "missing", default)
```这返回`default`仅当查找引发时`AttributeError`。
它不会抑制来自描述符或自定义查找挂钩的任意异常。
## 33.29 属性查找和`setattr`
`setattr`执行动态属性分配。```python id="j47b3p"
setattr(obj, "x", 10)
```相当于:```python id="1q0s4t"
obj.x = 10
```为静态名称。
它仍然尊重:```text id="amsxsl"
__setattr__
data descriptors
slots
read-only attributes
```所以`setattr`不是原始字典写的。
## 33.30 属性查找和`delattr`
`delattr`执行动态属性删除。```python id="xe7qva"
delattr(obj, "x")
```相当于:```python id="mbgk3j"
del obj.x
```它仍然尊重:```text id="gnvxp9"
__delattr__
descriptor __delete__
slot deletion
instance dictionary deletion
```## 33.31 属性查找和内联缓存
属性查找很频繁,所以CPython对其进行了优化。
重复访问:```python id="xtr2dq"
for obj in objects:
total += obj.value
```可能会多次点击相同的属性布局。
CPython 可以将内联缓存数据附加到字节码指令。缓存可以存储以下事实:```text id="ooumb4"
expected receiver type
type version tag
dictionary version
descriptor result
slot offset
instance dictionary offset
```关于后来的处决:```text id="ylpgjg"
if guards still hold:
use fast path
else:
fall back to generic lookup
```这保留了动态语义,同时加速了稳定的情况。
## 33.32 缓存失效
Python 允许类突变。```python id="uqb4tq"
class C:
x = 1
obj = C()
print(obj.x)
C.x = 2
print(obj.x)
```第二次查找必须看到`2`。
因此属性缓存不能盲目重用旧结果。它必须防范变化。
典型的警卫可能涉及:```text id="1kbbj7"
type identity
type version
dictionary version
descriptor kind
instance layout
```当假设失败时,CPython 会回退到通用查找并可能更新缓存。
## 33.33 实例布局稳定性
当对象布局稳定时,属性缓存效果最佳。
例子:```python id="cf08rl"
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
points = [Point(i, i + 1) for i in range(1000)]
for p in points:
p.x
```每个`p`具有相同的类型和可能相似的属性布局。这是专业化的一个很好的例子。
动态更改会降低缓存有效性:```python id="nb80hw"
p.z = 10
del p.x
Point.x = property(...)
```解释者必须首先保持正确性。
## 33.34 属性查找和字典
实例字典经过高度优化。
CPython 字典是对象命名空间的核心:```text id="hr03df"
module namespace
class namespace
instance namespace
globals
builtins
```对于许多对象,属性查找最终变成字典查找加上描述符处理。
属性缓存通常依赖于字典版本控制,因此解释器可以判断名称空间是否已更改。
## 33.35 属性查找和模块
模块全局变量也是基于字典的。```python id="z6frja"
import math
math.sqrt
```模块对象将属性存储在其字典中。
在许多情况下,模块查找比实例描述符查找更简单,但是模块可以通过以下方式自定义缺失的属性行为`__getattr__`。
导入还绑定模块属性:```python id="3y1uib"
import package.submodule
```导入后,包可能会有一个`submodule`属性。
## 33.36 属性查找和内置函数
内置类型通常使用专门的内部布局和描述符。
例子:```python id="pswgfz"
list.append
dict.get
str.upper
int.bit_length
```这些属性通常是用 C 实现的方法描述符。
通过实例访问:```python id="c2qhto"
[].append
```返回一个绑定的内置方法对象。
立即调用可以使用优化的方法调用路径。
## 33.37 属性查找和内存管理
属性查找操作引用。
返回属性时,CPython 必须确保结果保持活动状态。
如果查找创建绑定方法、属性结果或描述符结果,则必须正确处理引用所有权。
查找期间可能会创建临时对象:```text id="yd6imc"
bound methods
property return values
descriptor return values
exception objects
strings or proxy objects in custom hooks
```故障路径必须清理它们。
由于属性查找可以调用Python代码,因此引用安全性至关重要。
## 33.38 属性查找和重入
属性查找可以重新进入Python。
例子:```python id="cjrgl2"
class C:
def __getattribute__(self, name):
return compute_value(name)
```致电给`compute_value`可以执行任意Python代码。
这意味着在一次属性查找期间,Python 代码可以:```text id="pbs3p5"
mutate the object
mutate the class
change descriptors
trigger garbage collection
raise exceptions
call back into the same object
```查找实现必须容忍这种情况。
## 33.39 属性查找和代理
代理对象通常实现自定义属性查找。```python id="gmm0jz"
class Proxy:
def __init__(self, target):
self._target = target
def __getattr__(self, name):
return getattr(self._target, name)
```然后:```python id="t9wo3d"
proxy.x
```代表:```python id="mwraoc"
target.x
```健壮的代理必须仔细处理特殊方法,因为隐式特殊方法查找通常会绕过正常的实例属性查找。
例如,实施`__getattr__`独自一人可能无法做到`len(proxy)`委托给`len(target)`。
## 33.40 属性查找和 ORM
许多 ORM 使用描述符。
形状示例:```python id="9lk1n2"
class Field:
def __get__(self, obj, cls):
return obj._data[self.name]
def __set__(self, obj, value):
obj._data[self.name] = value
class User:
name = Field()
```然后:```python id="gygh09"
user.name
```不读取普通属性。它调用`Field.__get__`。
这允许库实现:```text id="m7h3nv"
validation
lazy loading
database column mapping
change tracking
computed fields
relationship loading
```描述符将属性语法转换为可编程访问。
## 33.41 属性查找和数据类
数据类不会从根本上改变属性查找。```python id="itowya"
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
```除非启用了槽,否则实例通常将字段存储在实例字典中。```python id="2es6x4"
p = Point(1, 2)
print(p.x)
```这是普通的实例属性查找。
带插槽:```python id="zkk0iv"
@dataclass(slots=True)
class Point:
x: int
y: int
```字段存储使用槽而不是普通的实例字典。
## 33.42 属性查找和类型对象
类型是对象,它们也有属性。```python id="veiddt"
int.bit_length
str.upper
dict.items
```这些是类型对象的属性。
在实例上查找属性时,实例的类型控制查找。
当查找类的属性时,元类控制查找。
这种递归对象模型是 Python 的核心:```text id="7jbz3r"
object has type
type is also an object
type has metaclass
metaclass controls class attribute lookup
```## 33.43 属性查找和性能
属性访问比局部变量访问有更多的开销。
比较:```python id="2vbgf5"
x
```在函数内部,其中`x`是本地的:```text id="quibql"
LOAD_FAST
```和:```python id="h0qo06"
obj.x
```这需要:```text id="p6kgjq"
LOAD_FAST obj
LOAD_ATTR x
LOAD_ATTR可能涉及描述符逻辑、字典查找、缓存检查、类型版本检查和错误处理。
这就是为什么热循环有时会受益于本地绑定:```python id="3tqoir" append = xs.append for item in items: append(item)
## 33.44 热路径中的属性查找和错误
属性查找失败的代价相对较高,因为它会构造异常。
例子:```python id="uacsdx"
try:
value = obj.missing
except AttributeError:
value = default
```对于频繁错过的情况,使用字典或显式哨兵可能会更快。
但最好的设计取决于语义。当缺席是异常情况或与动态 API 交互时,属性查找失败是正确且惯用的。
## 33.45 检查属性查找
使用`vars`检查实例字典:```python id="pqoskd"
class C:
pass
obj = C()
obj.x = 1
print(vars(obj))
```使用类字典:```python id="zw539h"
print(C.__dict__)
```使用维护、恢复:```python id="oqmr19"
print(C.__mro__)
```使用`inspect.getattr_static`在许多情况下检查属性而不触发正常的动态查找:```python id="kb22m2"
import inspect
inspect.getattr_static(obj, "x")
```当描述符或`__getattr__`会执行代码。
## 33.46 最小属性查找模型
简化的查找模型:```python id="rjhnk7"
def lookup(obj, name):
cls = type(obj)
class_attr = find_in_mro(cls, name)
if is_data_descriptor(class_attr):
return class_attr.__get__(obj, cls)
if hasattr(obj, "__dict__") and name in obj.__dict__:
return obj.__dict__[name]
if has_get(class_attr):
return class_attr.__get__(obj, cls)
if class_attr is not missing:
return class_attr
getattr_hook = find_in_mro(cls, "__getattr__")
if getattr_hook is not missing:
return getattr_hook(obj, name)
raise AttributeError(name)
```这省略了重要的实际细节:```text id="vn6j5o"
custom __getattribute__
metaclasses
slots internals
C-level fast paths
reference counts
inline caches
error handling
module lookup
special method lookup
```但它捕获正常的描述符优先级顺序。
## 33.47 常见误解
|误会 |正确型号 |
|---|---|
|`obj.x`总是读`obj.__dict__["x"]`|数据描述符、槽、类属性和钩子可能会介入 |
|方法存储在每个实例上 |普通方法存储在类中并在查找期间绑定 |
|属性是字段 |属性是调用函数的描述符 |
|`__getattr__`处理每次查找|它仅在正常查找失败后运行 |
|`__getattribute__`仅处理缺失属性 |它处理所有正常的属性访问 |
|实例属性总是胜过类属性 |数据描述符击败实例属性 |
|特殊方法总是使用普通查找 |许多都是通过类型槽查找的 |
|属性访问没有副作用 |它可以执行任意Python代码 |
## 33.48 阅读策略
要研究属性查找,请按以下顺序构建示例:```python id="xp7o07"
class C:
x = 1
```然后添加:```python id="jdu7zp"
obj.x = 2
```然后添加一个方法:```python id="yzo90g"
def f(self): ...
```然后将其替换为:```python id="nku86v"
@property
def x(self): ...
```然后添加:```python id="6ho4rb"
__getattr__
__getattribute__
__slots__
staticmethod
classmethod
multiple inheritance
metaclass
```对于每个步骤,检查:```python id="eaahv5"
vars(obj)
C.__dict__
C.__mro__
type(obj)
```并反汇编访问站点:```python id="xhk4lr"
import dis
def read(obj):
return obj.x
dis.dis(read)
```这构建了从语法到对象模型行为的实用映射。
## 33.49 章节总结
属性查找是背后的机制`obj.name`。它是一个动态协议,涉及对象类型、类字典、实例字典、描述符、继承、自定义挂钩、槽、模块、元类和内联缓存。
普通实例属性的核心查找优先级是:```text id="t5lrtf"
data descriptor
instance dictionary
non-data descriptor
class attribute
__getattr__
AttributeError
```这个顺序解释了方法、属性、阴影、槽和许多框架模式。
CPython 通过专门的字节码路径和内联缓存大力优化属性查找,但它必须保留 Python 的动态语义。类可以更改,实例可以更改,描述符可以运行代码,自定义挂钩可以覆盖整个过程。