#28. 框架

一帧是一次主动执行 Python 代码的运行时记录。当 CPython 调用 Python 函数、执行模块主体、运行类主体、恢复生成器或恢复协程时,它使用类似帧的执行记录来保存当前状态。

代码对象说明要执行什么。

框架表示当前执行的位置以及当前存在的值。text code object = immutable instructions and metadata frame = mutable execution state for one run of that code 对于这个函数:```python def add(a, b): c = a + b return c


创建的框架`add(2, 3)`包含当前参数值、局部变量槽、堆栈值、指令位置、异常状态以及到执行上下文的链接。

## 28.1 框架为何存在

Python 程序可以同时有多个活动调用:```python
def a():
    return b()

def b():
    return c()

def c():
    return 42

a()
```在执行过程中,CPython 需要在被调用者运行时记住每个挂起的调用者。

从概念上讲:```text
frame for a
    waiting for b

frame for b
    waiting for c

frame for c
    currently executing
```每个帧都存储足够的状态,以便在嵌套调用返回后恢复其代码。

框架回答了这些问题:```text
Which code object is running?
Which instruction is next?
What are the local variables?
What temporary values are on the stack?
Which globals and builtins are visible?
What exception is being handled?
Is tracing or profiling active?
Who called this frame?
```## 28.2 代码对象与框架

代码对象是可重用的。一帧是一个执行实例。```python
def f(x):
    return x + 1

a = f(10)
b = f(20)
```两个调用使用相同的代码对象:```python
print(f.__code__)
```但每个调用都有自己的帧状态。

|概念 |代码对象|框架|
|---|---|---|
|可变性 |大部分是不可变的 |可变 |
|终身|往往长寿|通常一调用或暂停执行 |
|包含字节码|是的 |引用代码对象|
|包含当地人 |没有 |是的 |
|包含堆栈 |没有 |是的 |
|包含指令指针|没有 |是的 |
|跨通话共享 |是的 |没有 |

代码对象是程序片段。该帧是该片段的运行激活。

## 28.3 函数框架

函数调用为被调用者创建或初始化一个框架。```python
def square(x):
    return x * x

square(9)
```函数对象包含:```text
code object
globals
defaults
keyword defaults
closure cells
annotations
qualname
module name
```调用时,CPython 将函数对象与实际参数组合起来并创建帧执行状态。

从概念上讲:```text
function object
    code object
    globals

call arguments
    x = 9

new frame
    code = square.__code__
    globals = square.__globals__
    locals slot 0 = 9
    value stack = empty
    instruction pointer = start
```然后,评估循环运行该帧,直到它返回、引发、让出或挂起。

## 28.4 模块框架

模块主体也在框架中执行。

对于一个文件:```python
# app.py
x = 10

def f():
    return x
```CPython 将整个模块编译成代码对象。运行该文件会在模块命名空间中执行该代码对象。

从概念上讲:```text
module frame
    code = compiled app.py
    globals = module.__dict__
    locals = module.__dict__
```在模块范围内,全局变量和局部变量通常引用相同的字典。

这就是顶级赋值写入模块命名空间的原因:```python
x = 10
```创建:```text
module.__dict__["x"] = 10
```函数的执行方式不同。函数局部使用快速本地槽,而不是模块字典作为主存储。

## 28.5 级车身框架

类体也作为代码执行。```python
class User:
    kind = "human"

    def name(self):
        return "anonymous"
```类语句并不简单地声明静态结构。 CPython 在临时命名空间中执行类主体,然后从该命名空间构建类对象。

从概念上讲:```text
prepare class namespace
execute class body frame
collect assignments and function objects
call metaclass to create class object
bind class object to name User
```在类主体执行期间:```python
kind = "human"
```存储到类命名空间中。

嵌套函数:```python
def name(self):
    return "anonymous"
```创建一个函数对象并将其存储在类命名空间中。它不运行方法体。

类体完成后,CPython 将命名空间传递给元类,通常`type`

## 28.6 框架内容

CPython 类似框架的执行记录包含几类数据。

|类别 |目的|
|---|---|
|代码参考|正在执行的代码对象 |
|全局变量 |用于名称查找的全局命名空间 |
|内置函数 |内置命名空间后备 |
|当地人 |局部变量或命名空间 |
|快当地人 |函数局部变量的槽数组 |
|价值栈|字节码的临时操作数 |
|指令状态 |当前字节码位置 |
|异常状态|主动异常处理元数据|
|来电关系 |链接到上一帧或调用上下文 |
|追踪状态 |调试器、分析器、覆盖范围挂钩 |
|业主状态 |生成器、协程或正常调用状态 |

简化的结构如下所示:```text
frame
    code object
    previous frame
    globals dictionary
    builtins dictionary
    locals representation
    fast local slots
    value stack
    instruction pointer
    exception state
    tracing flags
```确切的 C 结构因 CPython 版本而异。概念领域保持稳定。

## 28.7 快速局部变量

函数局部变量被优化。

在这个函数中:```python
def f(a, b):
    c = a + b
    return c
```编译器知道本地名称:```text
a
b
c
```它为它们分配索引:

|名称 |插槽|
|---|---:|
|`a`| 0 |
|`b`| 1 |
|`c`| 2 |

在执行过程中,CPython 可以通过槽访问局部变量:```text
LOAD_FAST 0
LOAD_FAST 1
STORE_FAST 2
LOAD_FAST 2
```这避免了普通函数局部变量的字典查找。

框架将这些值存储在类似数组的区域中:```text
fast locals
    slot 0: a
    slot 1: b
    slot 2: c
```这是局部变量比全局变量更快的原因之一。

## 28.8 当地人词典

Python 通过以下方式公开局部变量`locals()`和框架属性。```python
def f(a):
    b = a + 1
    print(locals())

f(10)
```输出:```text
{'a': 10, 'b': 11}
```但在函数内部,实际执行存储是快速局部变量。根据上下文和 Python 版本,字典视图可以具体化或与内部存储同步。

这种区别很重要:```python
def f():
    x = 1
    locals()["x"] = 2
    return x
```您不应该依赖于修改返回的字典`locals()`更改函数内优化的局部变量。

在模块范围内,情况有所不同:```python
locals()["x"] = 2
print(x)
```在模块级别,局部变量是模块字典,因此这通常有效。

## 28.9 全局变量和内置函数

框架存储对用于名称解析的全局和内置命名空间的引用。

为了:```python
def f(xs):
    return len(xs)

len不是局部变量。 CPython 使用全局查找:text look in function globals if missing, look in builtins if missing, raise NameError 该框架提供了两个字典:text frame.globals = module namespace frame.builtins = builtins namespace 从概念上讲:python def f(xs): return len(xs) 运行为:```text LOAD_GLOBAL len LOAD_FAST xs CALL 1 RETURN_VALUE


`LOAD_GLOBAL`取决于框架的全局变量和内置变量。

## 28.10 值栈

每个帧都有一个供字节码执行使用的值堆栈。

为了:```python
def f(a, b, c):
    return (a + b) * c
```堆栈的变化大致如下:

|说明 |堆栈之前 |堆栈之后 |
|---|---|---|
|`LOAD_FAST a` | `[]` | `[a]` |
| `LOAD_FAST b` | `[a]` | `[a, b]` |
| `BINARY_OP +` | `[a, b]` | `[a + b]` |
| `LOAD_FAST c` | `[a + b]` | `[a + b, c]` |
| `BINARY_OP *` | `[a + b, c]` | `[(a + b) * c]` |
| `RETURN_VALUE` | `[result]`|返回 |

堆栈存储对象引用,而不是原始的未装箱值。

因此,对于整数,堆栈保存指向 Python 整数对象的指针。```text
value stack
    PyObject* -> int object
    PyObject* -> int object
```字节码指令压入、弹出、替换或检查这些堆栈条目。

## 28.11 指令位置

帧跟踪当前字节码位置。

对于直线代码,指令指针前进。

对于树枝,它会跳跃。```python
def abs_like(x):
    if x < 0:
        return -x
    return x
```概念控制流程:```text
load x
load 0
compare <
jump if false to return_x
load x
unary negative
return
return_x:
load x
return
```该帧记录下一条指令。这个职位对于以下方面很重要:```text
normal execution
branches
loops
exceptions
tracebacks
line tracing
profiling
debugging
```回溯使用帧状态来报告异常发生的位置。

## 28.12 框架和回溯

当异常传播时,CPython 会记录帧中的回溯信息。

例子:```python
def a():
    b()

def b():
    c()

def c():
    1 / 0

a()
```回溯显示了活动调用链:```text
a
b
c
ZeroDivisionError
```每个回溯条目指的是一个帧和指令位置。这就是为什么 Python 可以显示源文件名、行号和函数名。

回溯不仅仅是一个字符串。它是运行时信息的结构化链。

## 28.13 从 Python 访问框架

Python 通过多个 API 公开框架对象。```python
import inspect

def f():
    frame = inspect.currentframe()
    print(frame.f_code.co_name)
    print(frame.f_locals)
    print(frame.f_globals is globals())

f()
````frame`对象公开诸如以下的字段:

|属性 |意义|
|---|---|
|`f_code`|代码对象|
|`f_locals`|本地命名空间视图 |
|`f_globals`|全局命名空间 |
|`f_builtins`|内置命名空间 |
|`f_back`|上一帧|
|`f_lineno`|当前源线|
|`f_trace`|追踪功能 |

您还可以使用:```python
import sys

frame = sys._getframe()
print(frame.f_code.co_name)
```这些 API 功能强大,但对实现敏感。使框架对象保持活动状态也可以使局部变量和对象保持活动状态。

## 28.14 帧寿命

大多数正常的功能框架都是短暂的。```python
def f():
    x = object()
    return 1

f()
```什么时候`f`返回后,它的框架可以被销毁或重用,并且它的本地引用可以被释放。

但在某些情况下,框架的寿命可能会更长:```text
tracebacks keep frames alive
generators suspend frames
coroutines suspend frames
debuggers inspect frames
profilers observe frames
closures may keep cells alive
manual references to frames keep them alive
```例子:```python
import inspect

saved = None

def f():
    global saved
    x = [1, 2, 3]
    saved = inspect.currentframe()

f()
```全球`saved`现在指的是框架。该框架指的是它的本地人。分配给的列表`x`可能会保持活动状态,因为框架仍然活动。

这是调试工具和异常处理代码中令人惊讶的内存保留的常见原因。

## 28.15 框架链

框架可以通过以下方式引用其调用者`f_back````python
import inspect

def outer():
    inner()

def inner():
    frame = inspect.currentframe()
    print(frame.f_code.co_name)
    print(frame.f_back.f_code.co_name)

outer()
```从概念上讲:```text
inner frame
    f_back -> outer frame
        f_back -> module frame
```该链可以实现回溯构建和堆栈检查。

这也意味着保留最里面的帧可以使调用者帧保持活动状态。```text
saved inner frame
    keeps outer frame
        keeps module frame references
```对于内存敏感的工具,必须有意释放帧引用。

## 28.16 发电机和悬挂框架

生成器在让出后保持执行状态。```python
def gen():
    x = 1
    yield x
    x = 2
    yield x
```调用该函数会创建一个生成器对象:```python
g = gen()
```身体并没有立即运转。恢复时:```python
next(g)
```该帧运行直到第一个`yield`

`yield`,框架保持暂停状态:```text
generator object
    suspended frame
        code object
        local x = 1
        instruction position after first yield
        value stack state
```下一次调用将从保存的指令位置继续:```python
next(g)
```这与正常的函数框架不同,后者在返回时结束。

## 28.17 协程和框架

协程使用与生成器相同的总体思想:可恢复执行状态。```python
async def fetch():
    data = await read()
    return data
````await`,协程可以挂起。其框架保留:```text
local variables
current instruction position
pending awaitable
exception state
return path
```稍后事件循环将恢复它。```text
coroutine object
    suspended frame or frame-like state
        locals
        stack
        instruction pointer
```因此,异步执行取决于帧暂停和恢复。它不是一个单独的语言运行时。

## 28.18 框架物化

现代 CPython 将内部执行帧与完整的 Python 可见帧对象区分开来。

解释器可以在紧凑的内部框架下运行以提高性能。仅当内省、跟踪、调试、回溯处理或 API(例如`sys._getframe()`

从概念上讲:```text
internal frame
    optimized execution record

Python frame object
    object exposed to Python code
    wraps or materializes execution state
```这种区别提高了性能,因为并非每个函数调用都需要用户代码可见的堆分配的 Python 框架对象。

概念模型保持不变:仍然存在执行状态。确切的表示可能有所不同。

## 28.19 框架和闭包

闭包需要对嵌套函数之间共享的变量进行特殊存储。

例子:```python
def outer():
    x = 10

    def inner():
        return x

    return inner
```变量`x`之后必须生存`outer`返回因为`inner`仍然使用它。

CPython 使用单元对象来处理这个问题。

从概念上讲:```text
outer frame
    x stored in cell

inner function
    closure references same cell
````outer`返回时,框架可以消失,但单元格仍然存在,因为返回的函数引用了它。```python
fn = outer()
print(fn())
```关闭并不能保留全部`outer`正常情况下帧存活。它使所需的单元变量保持活动状态。

## 28.20 单元格和自由变量

编译器对闭包变量进行分类。

|术语 |意义|
|---|---|
|单元格变量 |内部函数捕获的局部变量 |
|自由变量|此处使用但在外部作用域中定义的变量 |

例子:```python
def outer():
    x = 1

    def inner():
        return x

    return inner
```为了`outer`, `x`是一个单元格变量。

为了`inner`, `x`是一个自由变量。

您可以检查一下:```python
def outer():
    x = 1
    def inner():
        return x
    return inner

print(outer.__code__.co_cellvars)
print(outer().__code__.co_freevars)
```框架布局包括这些单元的存储,因此嵌套函数可以安全地共享变量。

## 28.21 框架和例外

帧存储异常处理状态。

为了:```python
def f(x):
    try:
        return 10 / x
    except ZeroDivisionError:
        return 0
```代码对象包含异常处理元数据。该帧存储使用该元数据所需的当前执行状态。

当除法引发异常时,解释器使用帧的指令位置来查找匹配的处理程序。

从概念上讲:```text
exception raised
    inspect current frame
    locate handler for current bytecode range
    adjust stack state
    jump to handler
```如果当前帧中不存在处理程序,则该帧将展开并且异常将传播到调用者帧。

## 28.22 框架和`finally`一个`finally`块还取决于精确的帧状态。```python
def f():
    try:
        return 1
    finally:
        cleanup()
````finally`即使函数正在返回,块也会运行。

框架必须记住,在执行清理代码时,返回处于挂起状态。

从概念上讲:```text
start return with value 1
enter finally block
call cleanup
if cleanup succeeds:
    complete original return
if cleanup raises:
    replace return with new exception
```这就是为什么异常和返回状态是帧执行的一部分,而不是简单的事后想法。

## 28.23 帧和跟踪

追踪钩在框架上运行。```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)
```跟踪函数接收当前帧。它可以检查代码、局部变量、全局变量、行号和调用关系。

跟踪对于以下用途很有用:```text
debuggers
coverage tools
teaching tools
profilers
runtime monitors
```但追踪是有代价的。它迫使解释器在更多执行点保留和公开更多状态。

## 28.24 框架和分析

分析与跟踪类似,但通常比跟踪更粗糙。```python
import sys

def profile(frame, event, arg):
    print(event, frame.f_code.co_name)

sys.setprofile(profile)

def f():
    return 1

f()
sys.setprofile(None)
```分析事件包括调用和返回。分析器使用帧数据将时间或调用计数归因于函数。

该框架提供了从运行时执行到源代码级程序结构的映射。

## 28.25 帧和递归

递归调用为同一代码对象创建多个框架。```python
def fact(n):
    if n <= 1:
        return 1
    return n * fact(n - 1)
```为了`fact(4)`:

```text
fact(4)
    fact(3)
        fact(2)
            fact(1)
```每个调用都有自己的本地`n````text
frame fact: n = 4
frame fact: n = 3
frame fact: n = 2
frame fact: n = 1
```所有帧都引用相同的代码对象,但它们的本地槽不同。

CPython 跟踪递归深度,以防止无限制的递归调用耗尽较低级别的资源。

## 28.26 帧和内存保留

框架可以保留比预期更多的内存。

例子:```python
def load_big():
    data = bytearray(100_000_000)
    raise RuntimeError("failed")

try:
    load_big()
except RuntimeError as exc:
    saved = exc
```异常可以保留回溯。回溯可以保留帧。框架可以保留局部变量。所以`data`可能还活着。

常见的清理模式是:```python
try:
    load_big()
except RuntimeError as exc:
    handle(exc)
    exc = None
```或者避免存储异常的时间超过所需的时间。

保留链如下所示:```text
exception
    traceback
        frame
            locals
                large object
```理解框架有助于解释这种行为。

## 28.27 框架对象是内省的,不是免费的

框架内省功能强大,但有成本。

诸如此类的操作可以强制创建或同步框架对象:```python
inspect.currentframe()
sys._getframe()
locals()
traceback inspection
debugger hooks
coverage tracing
```这可能会影响:```text
performance
memory lifetime
local variable synchronization
optimizer freedom
debugging visibility
```对于正常的应用程序代码,避免在热路径中进行帧自省。对于调试器、分析器和运行时工具来说,框架内省是必不可少的。

## 28.28 简化的帧执行

简化的函数执行模型:```c
PyObject *
run_function(PyFunctionObject *func, PyObject **args, int nargs)
{
    Frame frame;

    frame.code = func->code;
    frame.globals = func->globals;
    frame.builtins = get_builtins(func->globals);
    frame.localsplus[0] = args[0];
    frame.localsplus[1] = args[1];
    frame.stack_pointer = frame.stack;
    frame.instruction_pointer = frame.code->first_instruction;

    return eval_frame(&frame);
}
```这省略了许多实际细节:```text
keyword arguments
defaults
closures
cell variables
free variables
generators
coroutines
exceptions
tracing
profiling
reference ownership
specialization
thread state
recursion checks
```但形状是准确的:函数调用准备一个框架,然后评估循环运行它。

## 28.29 简化的框架布局

一、教学布局:```c
typedef struct {
    CodeObject *code;
    DictObject *globals;
    DictObject *builtins;

    Object **localsplus;
    Object **stack_pointer;

    Instruction *instruction_pointer;

    ExceptionState exception_state;

    struct Frame *previous;
} Frame;
```重要的想法是,本地槽和堆栈值通常存储在彼此附近,以实现高效执行。

从概念上讲:```text
frame memory
    fixed metadata
    locals and cells
    value stack
```编译器计算需要多少个本地槽和堆栈槽。

## 28.30 Localsplus 区域

CPython 在其执行帧中使用局部变量和类似堆栈的数据的组合区域。

概念布局:```text
localsplus
    fast locals
    cell variables
    free variables
    value stack
```为了:```python
def f(a, b):
    c = a + b
    return c
```布局可以理解为:```text
localsplus
    [0] a
    [1] b
    [2] c
    [3...] value stack area
```对于闭包,单元格和自由变量也占据代码对象已知的位置。

这种紧凑的布局减少了分配开销并提高了局部性。

## 28.31 帧状态转换

一个框架可以经历多种状态。```text
created
    
executing
    
returned
```生成器和协程添加更多状态:```text
created
    
suspended
    
executing
    
suspended
    
completed
```异常路径:```text
executing
    
exception raised
    
handler found
    
executing handler
```或者:```text
executing
    
exception raised
    
no handler
    
unwound
```帧状态决定哪些操作是合法的。已完成的发电机无法恢复。正在运行的发电机无法重新进入。

## 28.32 可重入执行

Python 执行可以是可重入的。

帧可以执行调用用户代码的指令,该指令在第一条指令完成之前创建另一个帧。

例子:```python
class X:
    def __add__(self, other):
        return 42

def f(a, b):
    return a + b

f(X(), X())
````BINARY_OP`指令中`f`来电`X.__add__`,它执行另一个 Python 框架。

从概念上讲:```text
frame f
    BINARY_OP
        calls __add__
            frame X.__add__
                return 42
    continue frame f
```这就是为什么 CPython 的评估模型必须在可能重新进入 Python  C 帮助程序调用之间保留帧状态。

## 28.33 帧和 C 堆栈

Python 框架和 C 堆栈相关但又不同。

Python 函数调用创建 Python 执行状态。根据 CPython 版本和调用路径,C 堆栈也可能会增长。

随着时间的推移,解释器一直致力于减少 Python 调用中不必要的 C 递归,但本机调用、扩展调用和一些运行时路径仍然涉及 C 堆栈。

重要区别:```text
Python frame
    Python-level execution state

C stack frame
    native machine call state
```Python 回溯显示 Python 帧,而不是解释器内的每个 C 堆栈帧。

## 28.34 帧清除

可以清除帧以释放引用。

一个框架引用了许多对象:```text
locals
globals
builtins
stack values
trace function
exception state
previous frame
```当不再需要框架时,CPython 必须释放拥有的引用,以便可以收集对象。

对于生成器和协程来说,清理更加精细,因为框架可能会被悬挂。关闭生成器必须安全地释放其框架状态。

例子:```python
def gen():
    data = bytearray(100_000_000)
    yield 1

g = gen()
next(g)
del g
```删除生成器可以释放悬挂框架,从而释放`data`,假设不存在其他参考。

## 28.35 帧检查示例

该程序打印一个简单的调用堆栈:```python
import sys

def print_stack():
    frame = sys._getframe()
    while frame is not None:
        print(frame.f_code.co_name, frame.f_lineno)
        frame = frame.f_back

def c():
    print_stack()

def b():
    c()

def a():
    b()

a()
```从概念上讲,该链是:```text
print_stack
c
b
a
<module>
```确切的行号取决于文件。

此示例展示了帧如何形成对 Python 代码可见的链接运行时堆栈。

## 28.36 框架设计权衡

框架位于表现和内省之间。

CPython 希望帧速度快,因为每个 Python 调用都使用它们。但 Python 还向用户代码和工具公开框架。

这会产生紧张:

|目标|压力|
|---|---|
|快速通话 |保持框架紧凑和内部 |
|调试|暴露丰富的框架对象|
|简介 |保留呼叫和线路元数据 |
|追溯|失败后保持足够的状态 |
|发电机|支持悬挂|
|协程 |支持异步暂停 |
|内存效率 |避免保留不必要的参考资料 |
|兼容性 |保留Python级别的框架API |

许多 CPython 框架工作都是为了平衡这些约束。

## 28.37 常见误解

|误会 |正确型号 |
|---|---|
|框架与代码对象相同 |一个框架执行一个代码对象 |
|每个函数对象都有一个框架 |每个活动呼叫都有单独的帧状态 |
|局部变量总是存在于字典中 |函数局部通常使用快速槽 |
|`locals()`永远是真正的存储|在函数中,可能是视图,也可能是同步映射 |
|回溯只是字符串 |回溯参考框架和代码位置 |
|发电机每次重新启动 |发电机恢复悬挂架 |
|闭合使整个外框架保持活力 |通常它们可以保持所需细胞的活力|
|框架内省是免费的 |它会影响性能和内存寿命|

## 28.38 实用阅读策略

要研究 CPython 中的框架,请从 Python 级别的行为开始。

使用:```python
import dis
import inspect
import sys
```研究一下这个函数:```python
def outer(x):
    y = x + 1

    def inner(z):
        return x + y + z

    return inner
```检查:```python
fn = outer(10)

print(outer.__code__.co_varnames)
print(outer.__code__.co_cellvars)
print(fn.__code__.co_freevars)
print(fn.__closure__)

dis.dis(outer)
dis.dis(fn)
```然后将输出映射到这些概念:```text
fast locals
cell variables
free variables
code objects
function objects
frames
stack effects
closure cells
```这给出了从 Python 语法到框架内部的具体路线。

## 28.39 章节总结

帧是 CPython 对正在运行的 Python 代码块的执行记录。它将代码对象绑定到实时运行时状态:局部变量、全局变量、内置变量、堆栈值、指令位置、异常状态和调用者上下文。

框架解释了函数调用、模块执行、类体、回溯、调试、分析、递归、生成器、协程、闭包和内存保留。

主要型号为:```text
code object
    immutable instructions and metadata

frame
    mutable execution state for one execution

evaluation loop
    runs the frame until return, exception, yield, or suspension
```理解框架使评估循环具体化。解释器不执行抽象源代码。它通过字节码指令推进帧状态。