27.评估循环
#27.评估循环
The evaluation loop is the central execution engine of CPython. It takes a compiled code object, executes its bytecode instructions, and produces a result or an exception.
At a high level, CPython execution looks like this:```text Python source ↓ tokens ↓ parser ↓ AST ↓ symbol table ↓ compiler ↓ code object ↓ frame ↓ evaluation loop ↓ Python result or exception
评估循环存在于 CPython 的解释器实现中。从历史上看,密钥文件一直是`Python/ceval.c`,周围的解释器机制分布在其他文件中。现代 CPython 还根据字节码定义生成一些解释器代码。细节在版本之间发生变化,但模型保持稳定:框架通过重复分派字节码指令来执行代码对象。 CPython 自己的开发人员指南指出内部文档和源代码树作为当前参考,因为此代码在不同版本之间发生变化。
## 27.1 评估循环的工作
评估循环不解析 Python 文本。它不构建 AST。它通常不决定词法范围。在执行开始时这些作业已经完成。
它的工作范围更窄,也更机械:```text
read the next bytecode instruction
decode its operand
perform the operation
update the frame
continue, jump, call, return, or raise
```例如这个函数:```python
def add(a, b):
return a + b
```被编译成代码对象。代码对象包含字节码。什么时候`add(2, 3)`被调用时,CPython 会为该调用创建或初始化一个框架,将参数存储在快速本地槽中,然后通过评估循环运行该框架。
循环最终到达返回指令。该指令弹出或读取返回值,展开帧,并将对象指针返回给调用者。
从概念上讲:```text
call add(2, 3)
create frame
store a = 2
store b = 3
execute LOAD_FAST a
execute LOAD_FAST b
execute BINARY_OP +
execute RETURN_VALUE
return 5
```真正的实现比较复杂,但这就是核心模型。
## 27.2 主要运行时对象
评估循环连接多个 CPython 运行时对象。
|运行时对象 |角色 |
|---|---|
|代码对象|不可变的编译字节码和元数据 |
|框架|一次调用的可变执行状态 |
|线程状态 |每线程解释器执行状态 |
|口译状态 |每个解释器运行时状态 |
| Python 对象 |由指令操作的运行时值 |
|类型对象 | Python 对象的运行时行为表 |
代码对象描述了应该运行的内容。
帧存储该代码的一次活动执行。
评估循环执行该帧。
这种区别很重要。单个代码对象可以执行多次。每个调用都有自己的帧状态。```python
def f(x):
return x + 1
a = f(10)
b = f(20)
```两个调用都使用相同的代码对象,但每个调用都有单独的局部变量、堆栈状态和返回值。
## 27.3 代码对象
代码对象包含 Python 代码的编译表示形式。
您可以从 Python 中检查一个:```python
def f(x):
y = x + 1
return y
code = f.__code__
print(code.co_name)
print(code.co_varnames)
print(code.co_consts)
print(code.co_names)
print(code.co_stacksize)
```典型的领域包括:
|领域|意义|
|---|---|
|`co_code`|字节码流,以版本相关的形式公开 |
|`co_consts`|代码使用的文字常量 |
|`co_names`|字节码引用的名称 |
|`co_varnames`|局部变量名 |
|`co_freevars`|从外部作用域捕获的自由变量 |
|`co_cellvars`|内部作用域捕获的变量 |
|`co_argcount`|位置参数计数 |
|`co_kwonlyargcount`|仅关键字参数计数 |
|`co_stacksize`| Required value stack size |
|`co_flags`|代码标志 |
|`co_filename`|源文件名 |
|`co_name`|函数或块名称 |
|`co_qualname`|合格名称|
|`co_firstlineno`|第一个源代码行 |
|线表|从字节码偏移量到源代码行的映射
|异常表|结构化异常处理元数据|
这`dis`模块专门用于检查 CPython 字节码。其文档指出,CPython 字节码是一个实现细节,可能会在 Python 版本之间发生变化。
## 27.4 框架
帧是一个执行记录。
当函数运行时,CPython 需要存储以下内容:```text
the code object being executed
the instruction pointer
local variables
temporary stack values
globals dictionary
builtins dictionary
closure cells
exception state
return state
tracing and profiling state
```该结构就是框架。
简化的框架模型如下所示:```text
frame
code object
globals
builtins
locals / fast locals
instruction pointer
value stack
block and exception state
previous frame / caller relation
```Python 调用链创建一系列框架:```python
def a():
return b()
def b():
return c()
def c():
return 42
a()
```从概念上讲:```text
frame for a
frame for b
frame for c
```在任何时刻,当前线程状态都指向当前正在执行的帧或等效的内部帧表示。
## 27.5 快速本地化
函数局部变量在执行期间通常不会存储在普通字典中。
CPython 使用类似数组的布局来实现快速局部变量。名称在编译时解析为本地索引。然后,字节码指令可以通过索引访问局部变量,而不是进行字典查找。
例子:```python
def f(a, b):
c = a + b
return c
```编译器分配本地槽:
|名称 |插槽|
|---|---:|
|`a`| 0 |
|`b`| 1 |
|`c`| 2 |
然后字节码可以使用基于槽的操作:```text
LOAD_FAST 0 load a
LOAD_FAST 1 load b
BINARY_OP +
STORE_FAST 2 store c
LOAD_FAST 2 load c
RETURN_VALUE
```这就是为什么局部变量访问通常比全局变量访问更快。本地访问可以使用直接帧时隙。全局访问必须搜索字典并处理内置回退。
## 27.6 价值堆栈
CPython 字节码使用堆栈模型。
大多数指令读取和写入帧本地值堆栈。该堆栈与 C 调用堆栈是分开的。它存储`PyObject *`字节码执行期间的值。
对于这个表达式:```python
x = (a + b) * c
```堆栈行为大致如下:```text
LOAD_FAST a stack: [a]
LOAD_FAST b stack: [a, b]
BINARY_OP + stack: [a_plus_b]
LOAD_FAST c stack: [a_plus_b, c]
BINARY_OP * stack: [product]
STORE_FAST x stack: []
```值栈是字节码设计的核心。它避免了需要每条指令来命名显式源寄存器和目标寄存器。相反,指令就堆栈效应达成一致。
一些指令推送值:```text
LOAD_CONST
LOAD_FAST
LOAD_GLOBAL
BUILD_LIST
```一些指令弹出值:```text
STORE_FAST
POP_TOP
RETURN_VALUE
```有些两者兼而有之:```text
BINARY_OP
CALL
LOAD_ATTR
COMPARE_OP
```## 27.7 指令指针
该帧跟踪字节码流内的执行位置。
对于直线代码,指令指针在每条指令后向前移动。
对于分支、循环、异常处理和返回,指令会更改控制流。
例子:```python
def sign(x):
if x < 0:
return -1
return 1
```概念字节码流程:```text
load x
load 0
compare <
jump if false to positive_return
load -1
return
positive_return:
load 1
return
```指令指针使这成为可能。分支指令改变下一条要执行的指令。
## 27.8 调度
调度是为当前字节码指令选择 C 实现的行为。
简化的解释器循环如下所示:```c
for (;;) {
opcode = read_opcode(frame);
oparg = read_operand(frame);
switch (opcode) {
case LOAD_FAST:
/* load local variable */
break;
case LOAD_CONST:
/* load constant */
break;
case BINARY_OP:
/* perform binary operation */
break;
case RETURN_VALUE:
/* return from frame */
break;
}
}
```这只是一种教学模式。现代 CPython 使用优化的调度技术并在适当的地方生成解释器代码。尽管如此,基本的形状仍然存在:```text
fetch
decode
dispatch
execute
repeat
```派遣成本很重要。每个 Python 字节码指令都会经过调度。如果一个循环执行数百万条字节码指令,调度开销就变得可见。
## 27.9 堆栈效应
每条字节码指令都有堆栈效应。
堆栈效应描述了指令消耗和产生多少值。
例如:
|说明 |输入堆栈|输出堆栈|
|---|---|---|
|`LOAD_CONST` | `[]` | `[const]` |
| `LOAD_FAST` | `[]` | `[local]` |
| `STORE_FAST` | `[value]` | `[]` |
| `BINARY_OP` | `[left, right]` | `[result]` |
| `RETURN_VALUE` | `[value]`|从框架返回 |
编译器必须知道堆栈效应才能计算代码对象所需的最大堆栈大小。该值显示为`co_stacksize`。
为了:```python
def f(a, b, c):
return (a + b) * c
```堆栈永远不需要保存超过两个或三个临时值,具体取决于确切的字节码。 CPython 记录所需的最大堆栈深度,以便框架可以保留足够的空间。
## 27.10 字节码操作数
许多字节码指令都有操作数。
操作数是附加到指令的小整数参数。含义取决于操作码。
示例:```text
LOAD_CONST 0 load co_consts[0]
LOAD_FAST 1 load fast local slot 1
STORE_FAST 2 store into fast local slot 2
LOAD_GLOBAL 3 load name from name table index 3
```字节码指令通常不存储指向对象或字符串名称的完整指针。它将索引存储到代码对象拥有的表中。
这使字节码保持紧凑,并将不可变元数据与执行状态分开。
## 27.11 运行一个简单函数
考虑:```python
def add(a, b):
return a + b
```反汇编可能因 Python 版本而异,但概念指令顺序是:```text
load local a
load local b
binary add
return value
```执行过程如下:
|步骤|说明 |堆栈之前 |堆栈之后 |
|---:|---|---|---|
| 1 |`LOAD_FAST a` | `[]` | `[a]`|
| 2 |`LOAD_FAST b` | `[a]` | `[a, b]`|
| 3 |`BINARY_OP +` | `[a, b]` | `[a + b]`|
| 4 |`RETURN_VALUE` | `[a + b]`|返回 |
在 C 级别,每个堆栈元素都是一个`PyObject *`。
为了`add(2, 3)`,堆栈保存指向Python整数对象的指针。加法操作通过 Python 对象语义进行调度。一般情况下,它不会直接发出 CPU 整数加法。
## 27.12 为什么`a + b`不仅仅是一条CPU指令
在Python中,`a + b`是动态的。
对象可以是整数:```python
1 + 2
```它们可能是字符串:```python
"hello " + "world"
```它们可能是列表:```python
[1] + [2]
```它们可能是用户定义的对象:```python
class X:
def __add__(self, other):
return "custom"
X() + X()
```用于加法的字节码指令必须遵循 Python 的数据模型。它必须检查操作数类型、找到正确的数字或序列运算、在需要时调用特殊方法、处理错误并返回 Python 对象。
所以评估循环不能处理`+`作为普通机器添加。它是对 Python 对象的动态操作。
现代 CPython 尽可能减少了这种开销。专门的自适应解释器可以在观察稳定的运行时行为后专门化操作。 PEP 659 将其描述为在行为变化时快速适应的小区域的专业化。
## 27.13 函数调用
函数调用是评估循环中最重要的路径之一。
为了:```python
result = f(x, y)
```口译员必须:```text
load callable f
load arguments x and y
arrange call arguments
check callable type
enter optimized call path if possible
create or initialize callee frame if it is a Python function
execute callee frame
receive return value
continue caller frame
```从概念上讲:```text
caller frame
LOAD_FAST f
LOAD_FAST x
LOAD_FAST y
CALL 2
create callee frame
run callee frame
return object
STORE_FAST result
```CPython 在调用方面花费了大量的优化工作,因为调用频繁且昂贵。重要机制包括:```text
vectorcall
fast locals
specialized call bytecodes
inline caches
frame optimizations
reduced temporary tuple/dict creation
```目标是避免不必要的参数堆积。从历史上看,许多调用都需要为参数构建元组和字典。现代调用路径尽可能在类似数组的布局中传递参数。
## 27.14 从框架返回
返回指令结束当前帧。
为了:```python
def f():
return 42
```返回指令产生一个`PyObject *`结果并展开框架。
调用者接收该对象作为调用表达式的结果:```python
x = f()
```从概念上讲:```text
callee frame stack: [42]
RETURN_VALUE
pop result
finish callee frame
give result to caller
caller resumes with stack: [42]
STORE_FAST x
```框架可以通过多种方式完成:
|退出路径|意义|
|---|---|
|正常返回 |函数返回一个值 |
|例外|函数通过引发 | 退出
|发电机产量 |框架暂停,稍后恢复 |
|协程等待 |协程暂停 |
|致命错误 |运行时级故障 |
评估循环必须处理所有这些路径。
## 27.15 例外情况
异常是正常解释器控制流的一部分。
为了:```python
def div(a, b):
return a / b
```如果`b`为零,除法运算产生`ZeroDivisionError`。
字节码指令不返回正常结果。相反,它设置异常状态并将控制权转移给异常处理逻辑。
从概念上讲:```text
execute BINARY_OP /
operation fails
set current exception
search exception table
jump to handler or unwind frame
```现代 CPython 使用与代码对象关联的结构化异常表。这些表描述了受保护的字节码范围和处理程序。这允许解释器在发生异常时找到正确的处理程序。
例子:```python
try:
x = 1 / y
except ZeroDivisionError:
x = 0
```评估循环必须知道受保护的范围在哪里、处理程序从哪里开始以及处理程序需要什么堆栈状态。
## 27.16 循环和分支
Python 循环编译为跳转。
例子:```python
def count(n):
i = 0
while i < n:
i += 1
return i
```概念字节码形状:```text
i = 0
loop_start:
load i
load n
compare <
jump if false to loop_end
load i
load 1
add
store i
jump to loop_start
loop_end:
load i
return
```评估循环没有针对每个 Python 的特殊 C 级 while 循环`while`。它执行实现循环的字节码指令。
因此,Python 循环是外部解释器循环内的解释器循环:```text
C evaluation loop
executes Python loop bytecode
jumps backward many times
```这是Python 紧密循环成本高昂的原因之一。每次迭代可能执行许多字节码指令,并且每个字节码指令都有调度和动态对象开销。
## 27.17 迭代
一个`for`循环使用迭代协议。
例子:```python
for item in xs:
use(item)
```概念执行:```text
iterator = iter(xs)
loop:
item = next(iterator)
if StopIteration:
exit loop
use(item)
jump loop
```评估循环执行调用的指令`iter()`,调用迭代器的下一个操作,句柄`StopIteration`和分支。
这意味着Python级别`for`循环是基于协议的。它们适用于列表、元组、字典、文件、生成器、自定义迭代器和许多扩展类型,因为解释器通过对象协议槽进行调度。
## 27.18 属性访问
属性访问也是动态的。
为了:```python
value = obj.name
```解释器必须实现Python的属性查找规则:```text
look at object type
handle descriptors
look in instance dictionary if applicable
look in class dictionary and base classes
call custom __getattribute__ if present
fall back to __getattr__ if applicable
raise AttributeError if missing
```看似简单的表达可能涉及重要的机制。
现代 CPython 使用内联缓存和专门化来加速常见属性访问模式。例如,重复访问具有稳定形状的对象的相同属性可以避免一些重复的查找工作。
## 27.19 全局和内置查找
全局查找比本地查找更昂贵。
为了:```python
print(len(xs))
```名称如`print`和`len`除非在本地分配,否则不是局部变量。 CPython 通过全局和内置命名空间查找它们。
从概念上讲:```text
look in globals dictionary
if missing, look in builtins dictionary
if missing, raise NameError
```这就是为什么本地绑定在紧密循环中可以更快:```python
def slow(xs):
for x in xs:
len(x)
def faster(xs):
local_len = len
for x in xs:
local_len(x)
```现代 CPython 可以专门进行全局查找,因此这种旧的微优化不像以前那么普遍有用。尽管如此,潜在的区别仍然存在:本地槽比基于字典的名称查找更简单。
## 27.20 GIL 和评估循环
在传统的 CPython 运行时中,评估循环在当前线程持有全局解释器锁时运行。
GIL 保护解释器状态,包括引用计数和许多对象内部。评估循环定期检查是否应该删除 GIL、处理信号、处理挂起的调用或允许另一个线程运行。
这意味着字节码执行在解释器级别是协作的。线程通常不会永远持有 GIL。 CPython 具有允许在线程之间切换的调度检查。
实际后果:```text
one thread executes Python bytecode at a time per traditional interpreter
I/O operations may release the GIL
C extensions may release the GIL around long native work
CPU-bound Python threads do not normally execute bytecode in parallel
```较新的 CPython 工作包括自由线程构建和每个解释器的更改,但评估循环仍然是线程状态、待处理工作和字节码执行相遇的中心位置。
## 27.21 执行期间的引用计数
堆栈上的每个值都是一个具有所有权规则的 Python 对象指针。
评估循环必须仔细维护引用计数。当指令压入值、存储值、替换值或丢弃值时,它必须正确保留对象生存期。
例子:```python
x = a + b
```从概念上讲:```text
load a obtain reference to object a
load b obtain reference to object b
add produce new reference to result
store x bind result to local slot
discard temporaries
```不正确的参考管理会导致泄漏或过早销毁。
在 C 级别,这意味着精心放置的操作相当于:```c
Py_INCREF(obj);
Py_DECREF(obj);
```确切的实现通常使用专门的宏和所有权约定。但不变量很简单:对象在仍然可以使用时必须保持活动状态,并且当解释器不再拥有引用时必须将其释放。
## 27.22 错误信号
CPython 中的大多数 C 辅助函数都使用通用约定:```text
return a valid pointer or success code on success
return NULL or error code on failure
set an exception on failure
```评估循环检查这些结果。
简化示例:```c
PyObject *result = PyNumber_Add(left, right);
if (result == NULL) {
goto error;
}
```这`NULL`return 本身并不描述异常。异常存储在线程状态中。
这种模式随处可见:```text
call helper
if failed:
go to error path
else:
push or store result
```评估循环包含许多错误退出,因为几乎所有 Python 操作都可能失败:```text
allocation can fail
attribute lookup can fail
function call can fail
comparison can fail
iteration can fail
import can fail
descriptor code can fail
user-defined special method can fail
```## 27.23 待处理的调用、信号和异步事件
评估循环还充当运行时级别工作的安全检查点。
CPython 无法处理任意 C 指令边界处的每个信号或挂起事件。相反,它会记录需要注意的事项,并在评估期间在受控点进行检查。
示例:```text
signal handling
pending calls from C APIs
thread switching requests
async exception injection
tracing and profiling hooks
monitoring hooks
interrupt checks
```这使得口译员易于管理。求值循环成为 Python 执行注意到外部事件的地方。
## 27.24 跟踪和分析
Python 支持通过 API 进行跟踪和分析,例如:```python
sys.settrace(...)
sys.setprofile(...)
```这些钩子需要评估循环的配合。
循环必须发出如下事件:```text
call
line
return
exception
opcode, when enabled
```跟踪会使执行速度变慢,因为它添加了检查和回调调用。但它支持调试器、覆盖工具、分析器、教学工具和可观测系统。
单步执行 Python 代码的调试器取决于评估循环将字节码执行映射回源代码行的能力。
## 27.25 专门化自适应解释器
自 Python 3.11 以来,CPython 包含了一个基于 PEP 659 的专用自适应解释器。其想法是保持 Python 语义动态,同时使常见的稳定情况更快。 PEP 659 将专业化描述为对小区域的攻击性,并在运行时模式发生变化时进行适应。
解释器从通用字节码开始。当代码运行时,CPython 会观察行为,并可能用专门的形式替换或增强通用操作。
例如,通用二元运算可能会针对常见操作数类型进行优化:```text
generic BINARY_OP
observed int + int repeatedly
↓
specialized integer-add path
```对于属性访问:```text
generic LOAD_ATTR
observed same attribute layout repeatedly
↓
cached attribute access path
```对于全局查找:```text
generic LOAD_GLOBAL
observed stable globals and builtins dictionaries
↓
cached global lookup path
```专业化必须保持正确。如果假设失败,解释者就会后退或适应。
这与传统的完整 JIT 编译器不同。它仍然在解释器架构内运行。它专门用于字节码级执行路径,而不是在一般情况下将整个函数编译为本机机器代码。
## 27.26 内联缓存
内联缓存是与字节码指令相关的小块缓存存储。
解释器不是每次都重新计算查找信息,而是将事实存储在需要它们的指令附近。
示例缓存信息可能包括:```text
type version
dictionary version
attribute offset
resolved descriptor
global dictionary version
builtin dictionary version
specialized call target
```简化的属性缓存模型:```text
LOAD_ATTR name
cache:
expected type = User
type version = 123
attribute offset = 2
```在下一次执行时,CPython 可以检查该对象是否仍然与缓存的假设匹配。如果是,则使用快速路径。如果不是,则返回到通用路径。
内联缓存工作得很好,因为给定源位置的字节码指令经常会重复看到相同类型的对象。
## 27.27 为什么专业化是安全的
Python 是动态的,因此必须保护专业化。
仅当其假设成立时,专用路径才有效。
例如:```python
obj.x
```如果 CPython 观察到稳定的对象布局,则可以进行专门化。但Python允许突变:```python
obj.__dict__["x"] = 10
type(obj).x = property(...)
obj.__class__ = OtherType
```因此,CPython 使用版本标签、防护、计数器和后备路径。
安全规则是:```text
use fast path only if guards prove assumptions still hold
otherwise use generic Python semantics
```这与许多动态语言运行时使用的广泛策略相同,但 CPython 使机器相对靠近字节码解释器。
## 27.28 生成的解释器代码
现代 CPython 并不将每个字节码实现都视为一个文件中手写的 switch case。
解释器的一部分是根据指令定义生成的。这有助于保持字节码元数据、堆栈效果、专业化信息和调度代码更加一致。
大致的想法:```text
instruction definitions
↓
generated opcode metadata
↓
generated dispatch support
↓
interpreter execution
```对于读者来说,这意味着事实来源可能并不总是最终生成的 C 文件。您经常需要检查指令定义文件、生成的标头和构建输出。
确切的文件和生成管道可能会因 CPython 版本而异,因此请使用您正在学习的版本的源代码树。
## 27.29 求值循环和 C 调用
求值循环经常调用 C 辅助函数。
示例:```text
PyNumber_Add
PyObject_GetAttr
PyObject_SetAttr
PyObject_Call
PyDict_GetItem
PyObject_RichCompare
PyIter_Next
```这些助手可以调用用户定义的 Python 代码。
例如:```python
a + b
```可以致电:```python
a.__add__(b)
```和:```python
obj.name
```可以致电:```python
obj.__getattribute__("name")
```因此求值循环可以间接地重新进入Python执行。字节码指令可以调用 C 帮助程序代码,后者可以调用 Python 代码,后者创建另一个帧,该帧启动另一个评估循环执行。
从概念上讲:```text
frame A
executes BINARY_OP
calls C helper
calls user __add__
frame B
evaluation loop
```这种递归执行模型是 Python 灵活性的核心。
## 27.30 递归和调用深度
Python 可以防止不受控制的递归。
例子:```python
def f():
return f()
f()
```每次调用都会创建另一个 Python 框架。 CPython 跟踪递归深度并提高`RecursionError`当超出配置的限制时。
评估循环和调用机制必须配合此检查。如果没有它,递归 Python 代码可能会耗尽 C 堆栈或进程内存。
您可以检查和调整限制:```python
import sys
print(sys.getrecursionlimit())
sys.setrecursionlimit(2000)
```提高递归限制应该小心谨慎。 Python 限制的存在部分是为了保护较低级别的运行时资源。
## 27.31 发电机
生成器改变了框架的生命周期。
正常的函数调用会一直运行,直到返回或引发。生成器可以暂停和恢复。
例子:```python
def gen():
yield 1
yield 2
```呼唤`gen()`不会立即运行函数体以完成。它创建一个拥有挂起帧或等效执行状态的生成器对象。
每个`next()`恢复执行:```text
first next()
enter frame
run until yield 1
suspend frame
second next()
resume frame
run until yield 2
suspend frame
third next()
resume frame
finish function
raise StopIteration
```评估循环必须支持暂停。它不能简单地破坏框架`yield`。
## 27.32 协程和等待
协程扩展了相同的悬挂模型。
例子:```python
async def fetch():
data = await read()
return data
```一个`await`可以挂起协程,直到另一个等待完成。
评估循环必须支持:```text
coroutine frame creation
suspension at await
resumption with value
resumption with exception
final return
cancellation behavior
```因此,异步执行不是一个单独的解释器。它建立在相同的框架和字节码机器上,具有用于挂起和恢复的特定指令和协议。
## 27.33 类体和模块体
求值循环不仅仅执行函数。
它还执行模块体和类体。
一个模块文件:```python
x = 1
def f():
return x
```被编译成模块级代码对象。导入或运行模块会执行该代码对象。
类语句还执行代码:```python
class C:
x = 1
def method(self):
return self.x
```类主体在为类构造准备的命名空间中运行。执行后,CPython 从该命名空间构建类对象。
因此评估循环执行几种块类型:
|块类|示例|
|---|---|
|模块|`.py`文件正文 |
|功能|`def f(): ...`|
|班级体|`class C: ...`|
|拉姆达 |`lambda x: x + 1`|
|理解力|`[x * 2 for x in xs]`|
|发电机|`(x for x in xs)`|
|协程 |`async def f(): ...`|
## 27.34 推导式
在许多情况下,推导式会编译为其自己的代码对象。
例子:```python
ys = [x * 2 for x in xs if x > 0]
```从概念上讲:```text
create list
iterate xs
for each x:
if x > 0:
append x * 2
return list
```这意味着推导式通常通过嵌套框架或专门的内部执行路径运行。它们有自己的局部作用域行为,这就是为什么列表推导式内的循环变量不会泄漏到 Python 3 中的周围作用域中。
求值循环将理解执行视为字节码执行,而不是特殊的语法形式。
## 27.35 导入执行
导入最终也会执行字节码。
当Python导入一个`.py`模块,导入系统找到模块,读取源或缓存的字节码,创建模块对象,然后执行模块代码对象。
从概念上讲:```text
import module
find spec
create module object
compile or load code object
execute code object in module namespace
```因此,评估循环参与导入。导入模块意味着运行代码。
这就是导入时副作用发生的原因:```python
# module.py
print("imported")
import module
```打印运行是因为模块主体执行是普通代码执行。
## 27.36 性能模型
评估循环解释了 Python 的大部分性能。
Python 操作通常有多层成本:```text
bytecode dispatch
stack manipulation
reference count updates
dynamic type checks
dictionary lookup
descriptor protocol
function call overhead
allocation
error checks
```例如:```python
obj.x + y
```可能需要:```text
LOAD_FAST obj
LOAD_ATTR x
LOAD_FAST y
BINARY_OP +
```每条指令都有解释器开销。`LOAD_ATTR`可能涉及描述符查找。`BINARY_OP`可能涉及数字调度。必须维护引用计数。必须检查错误。
这就是为什么将热循环转移到 C 扩展、矢量化库或内置操作中可以更快。它们减少了评估循环执行的字节码指令和动态调度的数量。
## 27.37 内置函数作为评估循环逃生口
内置操作可以在字节码级别以下执行大量工作。
例子:```python
sum(xs)
```评估循环执行调用`sum`,但元素上的循环可以在 C 语言的内置实现中运行。
比较:```python
total = 0
for x in xs:
total += x
```这需要每次迭代许多字节码指令。
内置函数可以减少解释器开销,因为大部分重复工作都发生在 C 中。
这是一个常见的Python性能原则:```text
fewer Python bytecode instructions in hot paths usually means better performance
```## 27.38 从 Python 检查评估循环
您可以使用以下命令来研究字节码`dis`:
```python
import dis
def f(a, b):
c = a + b
return c
dis.dis(f)
```您可以检查框架:```python
import inspect
def f():
frame = inspect.currentframe()
print(frame.f_code.co_name)
print(frame.f_locals)
f()
```您可以检查调用深度:```python
import sys
def f(n):
frame = sys._getframe()
print(n, frame.f_code.co_name)
if n:
f(n - 1)
f(3)
```您可以跟踪执行情况:```python
import sys
def trace(frame, event, arg):
print(event, frame.f_code.co_name, frame.f_lineno)
return trace
def f(x):
y = x + 1
return y
sys.settrace(trace)
f(10)
sys.settrace(None)
```这些工具公开了评估循环内部维护的部分机制。
## 27.39 简化的评估循环
循环的教学版本可能如下所示:```c
PyObject *
eval_frame(Frame *frame)
{
for (;;) {
Instruction instr = next_instruction(frame);
switch (instr.opcode) {
case OP_LOAD_CONST: {
PyObject *value = frame->code->consts[instr.arg];
push(frame, value);
break;
}
case OP_LOAD_FAST: {
PyObject *value = frame->locals[instr.arg];
if (value == NULL) {
raise_unbound_local_error();
goto error;
}
push(frame, value);
break;
}
case OP_STORE_FAST: {
PyObject *value = pop(frame);
frame->locals[instr.arg] = value;
break;
}
case OP_BINARY_ADD: {
PyObject *right = pop(frame);
PyObject *left = pop(frame);
PyObject *result = PyNumber_Add(left, right);
if (result == NULL) {
goto error;
}
push(frame, result);
break;
}
case OP_RETURN_VALUE: {
PyObject *result = pop(frame);
return result;
}
}
}
error:
return NULL;
}
```这省略了最真实的细节:```text
reference ownership
specialization
inline caches
exception tables
tracing
profiling
GIL checks
pending calls
signals
generators
coroutines
debug builds
statistics
opcode prediction
deoptimization
frame materialization
```但它抓住了基本思想。
## 27.40 常见误解
|误会 |正确型号 |
|---|---|
| CPython直接执行源文本 | CPython 执行编译后的代码对象 |
| Python 变量存储原始值 |名称和槽保存对对象的引用 |
|字节码跨版本稳定 |字节码是 CPython 实现细节 |
|`a + b`是简单的机器加法|它是动态对象协议调度,除非专门 |
|框架只是一个回溯对象 |帧处于活动执行状态 |
| GIL 只影响用户线程 |它与解释器执行和对象安全密切相关 |
|例外情况仅是小路 |异常被集成到正常的控制流机制中 |
|生成器只是特殊函数 |它们是可恢复执行帧或等效状态 |
## 27.41 阅读真实源代码
阅读真正的 CPython 源代码时,请使用以下顺序:
1. 开始于`dis`一个小型 Python 函数的输出。
2. 识别字节码指令。
3.找到对应的操作码定义。
4. 找到生成的或手写的解释器实现。
5. 遵循对象操作的助手调用。
6. 跟踪参考文献所有权。
7. 跟踪堆栈效果。
8. 跟踪错误路径。
9. 检查专门化和缓存行为。
10. 比较不同 Python 版本的行为。
一个好的学习函数是:```python
def example(obj, xs):
total = 0
for x in xs:
total += obj.value + x
return total
```该函数涉及许多解释器路径:```text
local variable access
loop iteration
attribute lookup
binary operation
in-place update semantics
jump instructions
return
```拆解它,然后将每条指令映射到解释器机器。
## 27.42 章节总结
评估循环是编译后的 Python 代码变成运行 Python 行为的地方。它通过帧执行代码对象,使用基于堆栈的字节码模型,调度指令,维护引用,处理异常,调用函数,检查运行时事件,并在可能的情况下应用专门化。
循环在概念上很小,但结果却很大。它位于几乎每个 CPython 子系统的交界处:```text
compiler
frames
objects
types
reference counting
garbage collection
exceptions
calls
imports
generators
coroutines
tracing
profiling
threading
optimization
```要理解 CPython,您必须了解求值循环。它是机器内部的机器。