32. 方法调用
32. 方法调用
方法调用是通常以属性访问开始的函数调用。它们是 Python 对象模型的核心,因为大多数对象行为都是通过方法公开的。
方法调用例如:python id="l3owku" obj.method(arg) 看起来像一个操作,但 CPython 执行几个概念步骤:```text id="j39rdj"
load obj
look up attribute method
bind obj if needed
load arg
call the resolved callable
return result or raise exception
## 32.1 方法调用是属性访问加调用
源表达式:```python id="xgyvgk"
obj.method(10)
```可以理解为:```python id="gg912h"
tmp = obj.method
tmp(10)
```这是正确的语义模型。首先进行属性查找。然后调用该查找的结果。
这个结果可能是:```text id="mfp3rd"
a bound method
a function
a built-in method
a callable object
a descriptor result
a property result
any object returned by __getattribute__
```仅当解析的值可调用时,调用才会成功。
例子:```python id="hsu4g1"
class C:
value = 10
obj = C()
obj.value()
```这失败了,因为`obj.value`决心`10`,并且整数不可调用。
## 32.2 存储在类中的普通函数
类内部定义的函数成为描述符。```python id="ys9rpg"
class User:
def greet(self, name):
return "hello " + name
```类字典包含一个函数对象:```python id="vcrq8f"
print(User.__dict__["greet"])
```通过实例访问时:```python id="moa6m1"
u = User()
u.greet
```函数的描述符行为将实例绑定为`self`。
从概念上讲:```text id="ho345m"
User.__dict__["greet"].__get__(u, User)
-> bound method
```绑定方法存储:```text id="3tilp4"
function object
self object
```呼叫:```python id="tnkuwp"
u.greet("Ada")
```在概念上等同于:```python id="zdsl7s"
User.greet(u, "Ada")
```自动`self`参数是函数调用和普通实例方法调用之间的主要区别。
## 32.3 绑定方法
绑定方法将函数与对象打包在一起。```python id="xz4yfy"
class C:
def f(self, x):
return x + 1
obj = C()
m = obj.f
print(m(10))
```这里,`m`是一个绑定方法。它记得`obj`。
从概念上讲:```text id="vqipye"
bound method
__func__ -> C.f
__self__ -> obj
```您可以检查一下:```python id="e0hgzo"
print(m.__func__)
print(m.__self__)
```什么时候`m(10)`被调用时,CPython 调用底层函数`obj`作为第一个参数插入:```python id="9rt8ja"
m.__func__(m.__self__, 10)
```## 32.4 通过类访问未绑定函数
通过类访问方法会提供类似函数的对象,该对象不会以相同的方式绑定实例。```python id="kyy416"
class C:
def f(self, x):
return x + 1
obj = C()
print(C.f(obj, 10))
```这里,`obj`是明确通过的。
类访问:```python id="0trmyu"
C.f
```不绑定特定实例`self`。
从概念上讲:```text id="xgfyue"
C.__dict__["f"].__get__(None, C)
-> function object or descriptor result suitable for class access
```所以这些对于普通的实例方法是等价的:```python id="1yhu3p"
obj.f(10)
C.f(obj, 10)
```第一种形式执行实例绑定。第二种形式显式传递实例。
## 32.5 描述符协议
方法绑定是描述符协议的一部分。
描述符是具有以下一种或多种方法的对象:```text id="02rkfq"
__get__
__set__
__delete__
```函数是非数据描述符,因为它们定义`__get__`。
当 CPython 计算时:```python id="8f0fmh"
obj.method
```并在类中找到一个函数对象,它调用该函数的描述符逻辑。
从概念上讲:```python id="q89qnh"
method = function.__get__(obj, type(obj))
```这会产生绑定方法。
描述符协议还支持:```text id="7keg5l"
methods
staticmethod
classmethod
property
slots
many built-in attributes
ORM fields
cached attributes
validation descriptors
```没有描述符就无法完全理解方法调用。
## 32.6 数据与非数据描述符
描述符分为两大类。
|描述符类型 |定义|优先|
|---|---|---|
|数据描述符|`__get__`和`__set__`或者`__delete__`|高于实例字典 |
|非数据描述符 |通常仅`__get__`|低于实例字典 |
存储在类上的普通函数是非数据描述符。
这意味着实例属性可以隐藏方法:```python id="8vtwg1"
class C:
def f(self):
return "method"
obj = C()
obj.f = lambda: "instance function"
print(obj.f())
```实例字典包含`f`,因此它胜过了类中的非数据描述符。
属性是一个数据描述符,因此它胜过实例字典:```python id="tfs80s"
class C:
@property
def x(self):
return 10
```查找规则影响最终调用的对象。
## 32.7 方法的属性查找顺序
对于典型的实例属性访问:```python id="ebj6os"
obj.name
```CPython 的查找过程大致如下:```text id="r7zrsx"
1. Determine type(obj)
2. Look for name in type and base classes
3. If a data descriptor is found, call its __get__
4. Otherwise, look in obj.__dict__ if present
5. If found in instance dictionary, return it
6. Otherwise, if a non-data descriptor was found, call its __get__
7. Otherwise, return the class attribute
8. Otherwise, call __getattr__ if defined
9. Otherwise, raise AttributeError
```方法调用取决于此查找。该方法不是仅通过名称来选择的。它是由完整属性查找协议选择的。
## 32.8 方法调用字节码
对于:```python id="fwr5qr"
def call(obj, x):
return obj.method(x)
```概念字节码如下所示:```text id="yqk6u7"
LOAD_FAST obj
LOAD_METHOD method
LOAD_FAST x
CALL 1
RETURN_VALUE
```确切的指令因 Python 版本而异,但现代 CPython 在常见情况下区分方法加载和通用属性加载。
目标是优化此模式:```python id="2kv28s"
obj.method(arg)
```而不改变语义结果。
## 32.9 为什么`LOAD_METHOD`存在
一个简单的方法调用会这样做:```text id="dg1ri1"
LOAD_FAST obj
LOAD_ATTR method
LOAD_FAST arg
CALL 1
```如果`method`是类中的一个普通函数,`LOAD_ATTR`创建一个绑定方法对象。
然后`CALL`立即调用它。
临时绑定方法分配通常是不必要的。
优化路径可以改为加载:```text id="eeqqg9"
underlying function
self object
```并直接调用该函数`self`插入。
从概念上讲:```text id="q3brdt"
obj.method(arg)
optimized:
function = C.__dict__["method"]
self = obj
call function(self, arg)
```这避免了在常见的立即调用情况下创建绑定方法对象。
## 32.10 避免绑定方法分配
考虑一个循环:```python id="v4zxb6"
for item in items:
obj.process(item)
```一个简单的实现可以为每次迭代分配一个绑定方法:```text id="th33f3"
obj.process -> new bound method
call bound method
discard bound method
```这会产生不必要的分配和引用计数流量。
CPython 的方法调用优化在常见情况下避免了这种情况。
语义模型仍然是:```python id="vxkr9d"
tmp = obj.process
tmp(item)
```但实施可能会跳过实现`tmp`当立即调用方法时作为堆对象。
## 32.11 当绑定方法仍然被创建时
当方法访问本身就是结果时,仍然会创建绑定方法对象。
例子:```python id="vnqqc0"
m = obj.method
```此处,Python 代码请求属性值。 CPython 必须生成绑定方法对象,因为程序可能会存储它、检查它、传递它或稍后调用它。```python id="ipgvev"
callbacks.append(obj.method)
```在这种情况下,绑定方法对象是正确的 Python 可见结果。
优化主要适用于绑定方法不需要转义的立即调用模式。
## 32.12 内置方法
内置类型公开了许多用 C 实现的方法。```python id="dkd1sx"
xs = []
xs.append(1)
```列表法`append`是用C实现的。
内置方法调用涉及:```text id="8n6a77"
method lookup
binding to list object
argument setup
C method call
mutation of list object
return None
```绑定对象在概念上仍然存在,但实现可以高度优化。
对于列表追加,该操作直接在 C 中改变底层列表结构。
## 32.13 方法描述符
内置方法通常由描述符对象而不是普通的 Python 函数对象表示。
例子:```python id="sfrzai"
print(list.__dict__["append"])
```这不是一个普通的 Python 函数。它是一个内置的方法描述符。
通过实例访问时:```python id="20nfof"
[].append
```它生成一个绑定到该列表的内置方法对象。
立即调用时:```python id="n6aps6"
[].append(1)
```CPython 可以遵循专门的本机路径。
## 32.14`staticmethod`
`staticmethod`禁用实例绑定。```python id="i095gy"
class Math:
@staticmethod
def add(a, b):
return a + b
Math.add(2, 3)
Math().add(2, 3)
```在这两种情况下,都没有`self`已插入。
描述符返回底层函数而不绑定实例。
从概念上讲:```text id="1s9dk7"
staticmethod.__get__(obj, cls)
-> original function
```所以这有效:```python id="opmkar"
Math().add(2, 3)
```因为该函数恰好接收两个参数,而不是三个。
## 32.15`classmethod`
`classmethod`绑定类而不是实例。```python id="dlj84i"
class C:
@classmethod
def make(cls, value):
return cls(value)
```呼叫:```python id="g1ck47"
C.make(10)
```通过`C`作为第一个参数。
通过实例调用:```python id="frurij"
obj = C()
obj.make(10)
```通常也通过了课程`C`, 不是`obj`。
从概念上讲:```text id="2cfzmr"
classmethod.__get__(obj, cls)
-> bound method with cls as first argument
```这就是为什么类方法对于替代构造函数和多态构造很有用。
## 32.16`property`和类似方法的访问
属性将方法逻辑转换为属性访问。```python id="b2eu4v"
class C:
@property
def value(self):
return 42
obj = C()
print(obj.value)
```这不调用`obj.value()`。
该调用发生在属性访问期间:```text id="m2zdkj"
obj.value
property.__get__(obj, C)
calls getter function
returns result
```如果该属性返回可调用对象,则可能会发生稍后的调用:```python id="ylxm9w"
obj.factory()
```如果`factory`是一个属性,这意味着:```text id="95d6z5"
call property getter
call returned object
```因此,一个源级调用可以在显式调用之前涉及隐藏描述符调用。
## 32.17`__getattribute__`每个正常的属性查找都会经过`__getattribute__`。```python id="e8vyww"
class C:
def __getattribute__(self, name):
print("lookup", name)
return super().__getattribute__(name)
def f(self):
return 1
obj = C()
obj.f()
```表达式`obj.f()`第一次通话`obj.__getattribute__("f")`。
这意味着方法调用可以被拦截。
定制`__getattribute__`能:```text id="xczvn9"
return a normal bound method
return a different callable
return a non-callable
raise AttributeError
log access
implement proxies
implement lazy loading
```调用机制不知道原始源意图。它调用查找返回的任何属性。
## 32.18`__getattr__`
`__getattr__`仅在正常查找失败后调用。```python id="zewpol"
class Dynamic:
def __getattr__(self, name):
if name == "run":
return lambda: "dynamic"
raise AttributeError(name)
obj = Dynamic()
print(obj.run())
```这里,`run`实例或类中不存在。`__getattr__`返回一个可调用的。然后该调用调用返回的可调用对象。
这常见于:```text id="xud82d"
proxies
RPC clients
ORM models
mock objects
lazy APIs
dynamic wrappers
```这也意味着方法调用可以在运行时动态解析。
## 32.19 模块上的方法调用
模块还可以支持模块级的动态属性访问`__getattr__`。```python id="m9w5tu"
# module.py
def __getattr__(name):
if name == "run":
return lambda: 42
raise AttributeError(name)
```然后:```python id="pkzy1x"
import module
module.run()
```可以动态解析。
查找路径与实例方法绑定不同,但源模式仍然是属性访问,然后调用。
## 32.20 方法解析顺序
对于类实例,方法查找使用方法解析顺序搜索类及其基类。```python id="pvvfui"
class A:
def f(self):
return "A"
class B(A):
pass
obj = B()
print(obj.f())
```查找搜索`B`, 然后`A`。
对于多重继承:```python id="f6sw15"
class A:
def f(self):
return "A"
class B:
def f(self):
return "B"
class C(A, B):
pass
```所选方法取决于`C.__mro__`。```python id="ig5h2o"
print(C.__mro__)
```MRO 是方法调用的核心,因为它决定类属性查找在哪里找到描述符。
## 32.21`super()`方法调用`super()`更改 MRO 中方法查找的开始位置。```python id="ujmrwo"
class Base:
def f(self):
return 1
class Child(Base):
def f(self):
return super().f() + 1
```通话:```python id="w4m10h"
super().f()
```并不意味着“按名称调用父类”。这意味着在当前类之后搜索 MRO,并绑定到当前实例。
从概念上讲:```text id="nlfcwi"
current class = Child
instance = self
MRO = [Child, Base, object]
search after Child
find Base.f
bind to self
call
```结果是使用同一实例的绑定方法。
## 32.22 方法和继承
方法调用可以使用从基类继承的方法。```python id="xtljqg"
class Base:
def save(self):
return "saved"
class User(Base):
pass
u = User()
u.save()
```查找发现`save`在`Base`,然后将其绑定到`u`。
从概念上讲:```text id="e2s1rr"
find Base.__dict__["save"]
bind with self = u
call Base.save(u)
```函数的定义类和实例的实际类可以不同。这是正常的。
## 32.23 重写方法
子类方法重写基类方法。```python id="3eakv8"
class Base:
def f(self):
return "base"
class Child(Base):
def f(self):
return "child"
print(Child().f())
```查找发现`Child.f`前`Base.f`。
这是运行时查找。该调用不受变量类型静态约束。```python id="43x5xb"
def call_f(obj):
return obj.f()
```选择的方法取决于`type(obj)`在运行时。
## 32.24 多态方法调用
Python 方法调用是动态调度的。```python id="uwb85l"
def speak(animal):
return animal.speak()
```不同的对象可以提供不同的实现:```python id="fcv0ik"
class Dog:
def speak(self):
return "woof"
class Cat:
def speak(self):
return "meow"
```相同的字节码可以根据运行时对象调用不同的方法。
从概念上讲:```text id="cje20f"
LOAD_FAST animal
LOAD_METHOD speak
CALL 0
```实际目标是在执行过程中发现的。
这种动态调度很灵活,但也带来了优化挑战。
## 32.25 单态和多态调用站点
如果方法调用站点通常看到一种对象类型,则该方法调用站点是单态的。```python id="j7bxa8"
for user in users:
user.validate()
```如果每一个`user`具有相同的类型,调用站点是单态的。
如果调用站点有多种类型,则该调用站点是多态的:```python id="kw1tz2"
for shape in shapes:
shape.area()
```在哪里`shape`或许`Circle`, `Square`, 或者`Triangle`。
当调用站点稳定时,内联缓存效果最佳。单态调用站点可以更有效地缓存类型和方法查找信息。
## 32.26 方法调用的内联缓存
CPython 可以在字节码指令附近缓存方法查找信息。
方法缓存可能会记录如下事实:```text id="cdvqx9"
expected receiver type
type version tag
resolved descriptor
method object or function pointer
offset or lookup result
call shape
```在下一次执行时:```text id="9o02ps"
if receiver type still matches
and type version is unchanged
use cached method path
else
fall back to generic lookup
```这可以在不改变语义的情况下加快重复调用的速度。
如果修改了类,则版本标记或缓存防护会使快速路径无效。
## 32.27 类突变和缓存失效
Python 允许类在运行时更改。```python id="jtw9f0"
class C:
def f(self):
return 1
obj = C()
print(obj.f())
def new_f(self):
return 2
C.f = new_f
print(obj.f())
```第二次调用必须使用新方法。
因此,任何采用旧方法的缓存都必须失效或受到保护。
安全规则:```text id="89t5m7"
fast method path is valid only while class and lookup assumptions remain true
```动态类突变是 CPython 优化使用防护的原因之一。
## 32.28 实例字典变异
实例属性还会影响非数据描述符的方法查找。```python id="4tnwq5"
class C:
def f(self):
return "class method"
obj = C()
obj.f = lambda: "instance value"
print(obj.f())
```实例字典条目隐藏了类函数,因为普通函数是非数据描述符。
方法缓存必须在相关时考虑实例字典状态。
这是方法查找比直接类表跳转更复杂的另一个原因。
## 32.29 槽和方法调用
课程有`__slots__`可能没有正常的实例字典。```python id="vhbgrn"
class C:
__slots__ = ("x",)
def f(self):
return self.x
```槽会影响属性存储,但方法查找仍然搜索类及其基类。
缺席`__dict__`可以简化一些属性情况,但描述符、继承和动态类突变仍然很重要。
## 32.30 特殊方法
特殊方法如`__len__`, `__add__`, 和`__iter__`通常通过类型槽而不是普通的实例属性查找来查找。
例子:```python id="k6wyb3"
len(obj)
```不只是执行:```python id="9dm1lp"
obj.__len__()
```in all respects. CPython 通常使用类型的槽来确定长度。
这种区别很重要:```python id="0t598r"
class C:
def __len__(self):
return 10
obj = C()
obj.__len__ = lambda: 20
print(len(obj))
print(obj.__len__())
```显式方法调用可以使用实例属性。这`len()`操作使用特殊方法通过类型查找。
特殊方法被优化并集成到对象协议槽中。
## 32.31 操作符调用与方法调用
运算符通常映射到特殊方法。```python id="3pof5j"
a + b
```可以致电:```text id="pzts11"
a.__add__(b)
b.__radd__(a)
```但CPython的表现并不普通`obj.__add__`每次添加的属性查找。它在类型对象上使用数字槽。
所以:```python id="rizu1b"
obj.method()
```和:```python id="g6n28n"
obj + other
```都是动态调度,但它们使用不同的内部路径。
方法调用使用属性查找和调用机制。操作员使用具有回退行为的协议槽。
## 32.32 方法调用和引用计数
方法调用必须保持活动状态:```text id="nb20se"
receiver object
resolved callable
arguments
temporary bound method, if created
return value
exception state, if raised
```对于避免绑定方法对象的优化方法调用,CPython 仍然需要确保接收者在底层函数运行时保持活动状态。
从概念上讲:```text id="7pxphh"
load receiver
resolve method
prepare self and args
call
release temporaries
push result
```这里不正确的引用处理可能会导致严重的错误,因为方法调用经常重新进入Python并且可以运行任意代码。
## 32.33 方法调用可以重新进入Python
方法查找本身可以执行Python代码。
示例:```text id="av2mft"
custom __getattribute__
descriptor __get__
property getter
__getattr__
metaclass attribute lookup
```然后解析后的方法调用可以执行更多的Python代码。
单一源表达式:```python id="1eq7mu"
obj.method(arg)
```可以涉及:```text id="44juqs"
call __getattribute__
call descriptor __get__
call method body
```每个调用都可以引发、改变状态或更改未来的查找行为。
## 32.34 方法调用和异常
方法调用可能在多个点失败:```text id="8b856f"
receiver expression raises
attribute lookup raises AttributeError or another exception
descriptor binding raises
argument expression raises
resolved object is not callable
argument binding fails
method body raises
return cleanup raises indirectly
```例子:```python id="q0kkqy"
obj.missing()
```属性查找期间失败。
例子:```python id="68k1hw"
obj.method(bad())
```在调用方法之前评估参数时可能会失败。
例子:```python id="ex1xt4"
obj.method()
```可能会在方法体内失败。
在所有情况下,字节码错误路径都必须清除临时堆栈值。
## 32.35 方法调用和`None`一个常见的错误:```python id="ov87fl"
xs = []
result = xs.append(1)
result.append(2)
list.append回报None。
第二行失败是因为result是None,不是列表。
在方法调用级别:text id="dx8gic" xs.append(1) mutates xs returns None 返回值仍然由call指令压入,然后存储在result。
方法调用并不意味着流畅的链接,除非方法显式返回self或另一个物体。
32.36 链式方法调用
链式调用从左到右执行。python id="jxorjs" obj.a().b().c() 从概念上讲:```text id="dylm9m"
tmp1 = obj.a()
tmp2 = tmp1.b()
tmp3 = tmp2.c()
如果`a()`回报`None`, 然后`.b()`失败。
字节码使用堆栈将每个中间结果保存足够长的时间以访问下一个方法。
## 32.37 流畅的 API
有些API故意返回`self`:
```python id="36isx5"
class Builder:
def set_name(self, name):
self.name = name
return self
def set_age(self, age):
self.age = age
return self
builder = Builder().set_name("Ada").set_age(37)
```每个方法都会改变对象并返回它。
方法调用机制是普通的。流畅的风格是库的约定,而不是特殊的解释器功能。
## 32.38 作为第一类对象的方法
方法可以存储和传递。```python id="63bqx2"
class C:
def f(self, x):
return x + 1
obj = C()
callback = obj.f
print(callback(10))
```绑定方法保留`obj`活。
从概念上讲:```text id="jrk395"
callback
function = C.f
self = obj
```这会影响内存寿命:```python id="ka89yy"
callbacks.append(obj.method)
```回调列表现在通过绑定方法使对象保持活动状态。
## 32.39 方法调用和垃圾收集
绑定方法可以参与参考图。
例子:```python id="7qj87e"
class C:
def f(self):
return 1
obj = C()
obj.callback = obj.f
```现在:```text id="7udx0g"
obj
-> callback bound method
-> self obj
```这就形成了一个循环。
如果这些循环变得无法访问并且终结规则允许清理,CPython 的循环垃圾收集器可以收集这些循环。
这是方法对象如何连接到内存管理的实际示例。
## 32.40 检查方法
您可以检查方法对象:```python id="dcn13l"
class C:
def f(self, x):
return x + 1
obj = C()
m = obj.f
print(type(m))
print(m.__func__)
print(m.__self__)
```您可以检查类字典内容:```python id="brp4q2"
print(C.__dict__["f"])
```您可以检查字节码:```python id="xq1plc"
import dis
def call(obj, x):
return obj.f(x)
dis.dis(call)
```这可以让您查看编译器是否为您的 Python 版本发出特定于方法的指令。
## 32.41 最小方法绑定模型
玩具描述符模型:```python id="b5ukc4"
class Function:
def __init__(self, code):
self.code = code
def __get__(self, obj, cls):
if obj is None:
return self
return BoundMethod(self, obj)
def __call__(self, *args):
return self.code(*args)
class BoundMethod:
def __init__(self, func, self_obj):
self.__func__ = func
self.__self__ = self_obj
def __call__(self, *args):
return self.__func__(self.__self__, *args)
```使用它:```python id="f8zqma"
def body(self, x):
return self.value + x
class C:
value = 10
C.f = Function(body)
obj = C()
print(obj.f(5))
```这不是 CPython 的实现,但它捕捉了绑定的思想:```text id="lybvyc"
function stored on class
access through instance
descriptor creates bound method
call inserts self
```## 32.42 常见误解
|误会 |正确型号 |
|---|---|
|`obj.method(x)`直接调用存储在的函数`obj`|它执行属性查找、绑定,然后调用 |
|`self`是按语法 | 插入的关键字`self`按照惯例只是第一个参数 |
|方法总是存在于实例中 |普通方法存在于类中并绑定到实例 |
|绑定方法总是被分配 | CPython 可以避免立即调用的分配 |
|`staticmethod`收到`self`|它不接收自动第一个参数 |
|`classmethod`接收实例 |它接收类|
|特殊方法总是像普通方法一样查找 |许多都是通过类型槽解决的 |
|方法查找是静态的 |它取决于运行时类型、MRO、描述符和实例状态 |
## 32.43 阅读策略
要研究方法调用,请从以下程序开始:```python id="fybmbx"
class C:
def f(self, x):
return x + 1
def call(obj):
return obj.f(10)
```检查:```python id="r4y4v3"
import dis
dis.dis(call)
obj = C()
m = obj.f
print(m.__func__)
print(m.__self__)
print(C.__dict__["f"])
```然后改变类:```python id="10svlx"
@staticmethod
def s(x): ...
@classmethod
def c(cls, x): ...
@property
def p(self): ...
```还测试:```python id="p178dp"
obj.f = lambda x: x * 2
```并检查查找如何变化。
这揭示了描述符、实例字典、字节码和调用机制之间的交互。
## 32.44 章节总结
方法调用是属性查找,然后是调用。对于普通实例方法,函数描述符将接收者对象绑定为第一个参数,生成一个绑定方法,在概念上相当于调用插入了实例的类函数。
核心模型是:```text id="xs3s0t"
obj.method(arg)
↓
lookup "method" on obj
↓
apply descriptor binding if needed
↓
obtain callable
↓
call callable with arguments
↓
return result or raise exception
```CPython 极大地优化了这条路径。它可以避免临时绑定方法分配、缓存方法查找、专门化稳定的调用站点以及通过快速 C 路径调用内置方法。
语义保持动态。运行时类型、MRO、描述符、实例字典、自定义属性挂钩、类突变和特殊方法规则都会影响实际调用的方法。