Chapter 1: Execution Model
执行模型回答一个直接问题:一段 Python 源码从文本进入运行时以后,解释器按什么单位执行,名字从哪些 namespace 查找,执行中的状态保存在哪里,调用、返回和异常如何改变下一步执行位置。读完本章后,你应能把一个短 Python 片段拆成 code block、execution context、frame、namespace、builtins 和 call stack,并据此判断它的执行顺序和名字解析结果。
本章以 Python 3.14 的 Execution model 作为语言语义边界,并以 CPython 的常见运行模型解释 frame 和 runtime state。这里的 CPython 细节只用于说明最常见实现路径;涉及内部对象布局、字节码格式和 frame 内存形态时,后续章节会按具体版本展开。
贯穿本章的代码片段如下。它同时包含 module 顶层、function body、class body、closure、builtins 查找和 frame 观察点,后续小节会反复回到这段代码。
# chapter1_probe.py
import inspect
name = "module-name"
def outer(label):
status = "outer-local"
def inner(value):
frame = inspect.currentframe()
return {
"value": value,
"label": label,
"status": status,
"local_keys": sorted(frame.f_locals),
"global_name": name,
"builtin_len": len([value]),
}
return inner
class Demo:
class_name = name
maker = outer("from-class-body")
snapshot = maker(7)
result = outer("from-module-body")(3)
这段代码的主线很短:module code block 从上到下执行;def outer 创建 function object;class Demo 执行 class body 并生成 class object;outer(...) 调用创建新的 function frame;inner(...) 调用再创建一个 frame;普通名字沿 local、enclosing、global、builtins 的路径解析。执行模型的核心价值在于,它让这些现象有统一解释。
1.1 code block 与 execution context
code block 是 Python 执行源码的基本单位。一个 module 文件、一个 function body、一个 class definition、交互式输入的一条命令、命令行 -c 传入的脚本、传给 eval() 或 exec() 的字符串,都可以成为 code block。语言参考把 code block 定义成“作为一个单元执行的 Python 程序文本”,这句话的工程含义是:编译、名字分类、执行 frame 和后续控制流都围绕 block 展开。
execution context 是运行某个 code block 时可用的执行环境。对本章来说,它至少包含五类对象:当前 frame、global namespace、local namespace、builtins namespace、异常状态。它还决定执行结束后返回哪里,例如函数调用返回调用方,module 顶层执行完结束进程或回到导入方,class body 执行完交给类对象创建逻辑。
在贯穿代码中,整个文件是 module code block。解释器执行到 name = "module-name" 时,会在 module namespace 中绑定 name。执行到 def outer(label): ... 时,函数体源码会被编译进一个 code object,并包装成 function object 绑定到 module namespace 的 outer。函数体本身此刻还没有运行,status = "outer-local" 也还没有建立对应的局部绑定。
执行到 class Demo: 时,class body 作为一个新的 code block 执行。这个 block 有自己的 local namespace,用来收集 class_name、maker、snapshot 这些名字。class body 执行完成后,运行时把这个 namespace 交给类创建逻辑,生成名为 Demo 的 class object,并把它绑定回 module namespace。
理解 code block 的检查点是:先找当前语句属于哪个 block,再看这个 block 对应的 local namespace、global namespace 和 builtins namespace。很多看似零散的 Python 行为,例如函数体延迟执行、class body 立即执行、exec() 写入外部字典,都能从这个检查点得到稳定解释。
1.2 module / function / class 的执行过程
module、function 和 class 都会引入 code block,但它们的执行时机和产物不同。module 顶层在加载脚本或导入模块时按顺序执行;function body 在函数被调用时执行;class body 在执行到 class statement 时立即执行,并在结束后生成 class object。
module 顶层执行的产物是一个 module namespace。贯穿代码的 name、outer、Demo、result 都属于这个 namespace。def outer 在 module 执行期间完成函数对象创建,所以 outer 这个名字可以被 class body 和后续 module 代码调用。
function body 的执行从调用开始。outer("from-module-body") 会创建 outer 的执行 frame,把实参绑定到形参 label,再执行 status = "outer-local" 和内部 def inner(value): ...。inner 是一个 function object,它携带自己的 code object,也携带对外层变量 label、status 的闭包关系。outer 返回 inner 后,outer 的普通执行已经结束,但被闭包引用的变量仍然可被 inner 后续读取。
class body 的执行更接近一段普通顶层代码,只是它使用类定义专用的 local namespace。class_name = name 会读取 module namespace 中的 name,并把结果写入 class local namespace。maker = outer("from-class-body") 在 class body 正在执行时调用 module namespace 中的 outer。snapshot = maker(7) 再调用返回的 inner,这个调用发生在类对象生成之前,但 inner 的普通名字查找规则仍按函数 frame、闭包、module globals 和 builtins 执行。
class body 的一个关键边界是:class namespace 会成为 class object 的属性字典来源;函数方法体的普通名字查找不会自动把 class namespace 当作 enclosing function scope。也就是说,class body 里创建的 class_name 会变成 Demo.class_name,但在方法体中直接写 class_name 并不会按闭包方式读取类属性。这个边界解释了很多类定义阶段和实例方法阶段的差异。
1.3 eval / exec 的动态执行模型
eval() 和 exec() 把字符串、code object 或 AST 重新放入编译与执行路径。eval() 面向表达式,返回表达式求值结果;exec() 面向 code block,执行语句序列,并把绑定结果写入给定 namespace。二者的共同点是:调用时可以显式提供 globals 和 locals,从而改变名字读取和写入的位置。
下面的片段展示动态 code block 如何使用外部提供的 namespace。它的重点是名字写入位置和 builtins 查找层级。
global_box = {"name": "dynamic-name"}
local_box = {}
exec("answer = len(name)", global_box, local_box)
value = eval("answer + 1", global_box, local_box)
exec() 执行的字符串中,answer = ... 是绑定操作。这里同时传入 global_box 和 local_box,所以 answer 写入 local_box。len(name) 的 name 会按动态执行时的名字解析规则读取,name 来自 global_box;len 来自 builtins namespace。随后 eval() 读取同一组 namespace,answer 从 local_box 找到,表达式求值得到 len("dynamic-name") + 1。
动态执行模型的工程后果是,源码文本可以在运行时进入新的编译单元,并用调用方给出的 namespace 执行。这会影响调试、沙箱、插件系统和配置脚本。判断 eval() 或 exec() 的行为时,稳定顺序是:确认输入是表达式还是语句块,确认传入的 globals 和 locals,确认名字读取层级,确认绑定结果写入哪个 namespace。
这里还要区分语言语义和 CPython 细节。普通代码应通过 builtins 模块显式访问内置对象;__builtins__ 是 CPython 为执行上下文连接 builtins namespace 时暴露出的实现细节。把它当成稳定公共接口,会让代码依赖实现形态。
1.4 frame 如何保存执行状态
frame 是 code block 正在执行时的状态容器。语言参考说 code block 在 execution frame 中执行;从读源码和调试的角度看,frame 把“执行到哪里、能看见哪些名字、异常从哪里传播”这些信息集中到一个对象关系里。
在 Python 层能观察到的 frame 属性包括 f_code、f_locals、f_globals、f_builtins、f_back、f_lineno。CPython 内部还需要保存 instruction pointer、value stack、fast locals 等执行器状态。Python 3.11 以后,CPython 的内部 frame 表示和解释器执行循环已经历过多轮优化;本章只使用公开可观察的 frame 属性建立执行模型。
贯穿代码中的 inspect.currentframe() 返回当前 inner 调用的 frame。这个 frame 的 f_code 指向 inner 的 code object;f_locals 能看到 value、frame 以及闭包变量的当前值;f_globals 指向 module namespace;f_builtins 指向 builtins namespace;f_back 指向调用 inner 的上一层 frame。sorted(frame.f_locals) 放入返回值,是为了把 frame 中可见的局部名暴露出来。
frame 还承担 traceback 的连接点。异常发生时,运行时会把异常对象和经过的 frame 连接成 traceback 链。调试器、inspect、trace hook 和异常保存都可能延长 frame 生命周期,所以“函数返回后 frame 立刻消失”只能作为普通路径的直觉,不能作为调试和异常场景下的判断依据。
读 frame 时要保持层级清楚:code object 描述可执行代码,frame 描述一次执行中的状态,function object 连接 code object、globals、defaults、closure 等调用所需信息。把三者混在一起,容易误判函数定义、函数调用和正在执行的调用帧之间的关系。
1.5 namespace 如何参与 name resolution
namespace 是名字到对象的映射。name resolution 是普通名字表达式读取对象的过程。对 function block 中的普通名字,常用检查顺序是 local、enclosing、global、builtins,也就是常说的 LEGB。这个顺序不是装饰性口诀,它决定 label、status、name、len 在贯穿代码中分别来自哪里。
在 inner(value) 内部,value 是 local,因为它来自当前函数的形参绑定。label 和 status 是 enclosing,因为它们在外层 outer 的 function block 中绑定,并被 inner 闭包捕获。name 是 global,因为当前函数和外层函数都没有绑定这个名字,函数对象的 globals 指向 module namespace。len 是 builtins,因为 local、enclosing、global 中都没有这个绑定,最终进入 builtins namespace。
名字解析和名字绑定要分开判断。status = "outer-local" 是绑定,它决定 status 属于 outer 的 local namespace。return status 这样的读取才进入 name resolution。Python 在编译阶段会扫描 block 内部的绑定操作,提前把名字分类为 local、free、cell、global 等;运行时 frame 再按分类选择读取位置。
class block 的名字解析有特殊边界。class body 可以读取 global 和 builtins,也可以在自己的 class local namespace 中绑定名字;class local namespace 在 class body 执行结束后成为 class object 的属性来源。函数 block 嵌套在 class block 中时,普通方法体不会把 class namespace 当成 enclosing function namespace。这个差异解释了类属性的稳定访问方式:通过 Demo.class_name、self.class_name 或描述符路径读取,方法体里的裸名字保留 function block 的名字解析规则。
当你排查 NameError、UnboundLocalError 或名字被覆盖的问题时,先确定当前语句属于哪个 code block,再找这个 block 中是否存在绑定操作,再沿对应 namespace 顺序读取。UnboundLocalError 的典型原因是:同一 function block 中存在对某名字的绑定,编译阶段把它归为 local,而读取发生在该 local 值建立之前。
1.6 builtins namespace 的最终查找层
builtins namespace 是普通名字查找的兜底层。len、open、Exception、object、range 等内置对象通过这一层进入 code block。它的位置很靠后:local、enclosing 和 global 都没有命中时,运行时才会查 builtins。
贯穿代码中的 len([value]) 展示了这个层级。inner 的 local namespace 没有 len,outer 的闭包变量里也没有 len,module namespace 中没有绑定 len,所以名字解析进入 builtins namespace,得到内置函数 len。如果 module 顶层写了 len = lambda items: 100,同一行代码会优先命中 global namespace 中的 len,内置 len 被遮蔽。
下面的片段展示遮蔽行为。它的目的不是建议这样写,而是让你看到 builtins 只在前面层级都没有命中时参与读取。
len = lambda items: 100
result = len([1, 2, 3])
这里的 result 会得到 100,因为 len 已经在 module namespace 中绑定。需要显式访问内置对象时,应读取 builtins 模块,例如 builtins.len([1, 2, 3])。这种写法把“我要访问内置对象”表达成明确依赖,也能让代码审查者立刻看到当前作用域里存在遮蔽风险。
builtins 的工程意义在于,它让常用对象不用逐个导入就能参与普通代码执行。但它也让名字解析多了一层隐式来源。阅读一段代码时,只要某个裸名字没有在当前局部、闭包和 module 中找到,就应检查它是否来自 builtins,或者是否因为遮蔽改变了原本预期。
1.7 call stack 与 execution order
call stack 记录当前线程里尚未完成的 frame 嵌套顺序。每次函数调用都会把新的 frame 推到当前调用链上;返回时当前 frame 退出,控制权回到调用方 frame;异常未被当前 frame 处理时,运行时沿调用链向外传播,并把经过的 frame 连接进 traceback。
贯穿代码的执行顺序可以用下面的图观察。图只表达本章关心的 block 与调用关系,省略对象内存布局、字节码细节和类型创建的底层路径。
图中的 D → E → F → G 是 class statement 的关键路径。class Demo 不是单纯登记一个类型名;执行到它时,class body 立即运行。class body 里的函数调用会正常进入 call stack,outer 和 inner 的 frame 都会短暂位于 class body frame 之上。等 class body 完成后,运行时才用收集到的 namespace 生成 Demo。
H → I 展示 module 顶层后半段的调用链。outer("from-module-body") 返回 inner,紧接着 (3) 调用这个返回值。执行 inner 时,当前 frame 可以读取自己的 local、闭包里的 label 和 status、module namespace 的 name、builtins 的 len。返回字典后,inner frame 退出,控制权回到 module frame,结果绑定到 result。
execution order 的判断顺序是:先按源码顺序找到当前正在执行的 statement,再判断该 statement 是否创建新对象、调用函数、执行 class body 或进入动态 code block,再沿 call stack 看控制权返回点。这个顺序能解释“为什么类体里的函数调用早于类对象完全可用”“为什么函数定义行执行了但函数体没有执行”“为什么异常 traceback 能显示多层调用位置”。
1.8 frame lifecycle 与 Runtime state
frame lifecycle 描述一次执行状态从创建到释放或被保留的过程。普通函数调用创建 frame;执行期间 frame 的局部变量、指令位置和异常状态持续变化;函数返回后,frame 通常会退出调用栈;如果异常 traceback、生成器、协程、调试器或 inspect 保存了它,frame 可以在普通调用结束后继续可观察。
不同执行形态会改变 frame 的生命周期。普通函数返回后,调用方只拿到返回值。异常发生时,frame 可能通过 traceback 链被异常对象间接持有。生成器和协程会在 yield 或 await 附近暂停,frame 的执行状态被保存,后续恢复时从暂停点继续。调试工具读取 frame 时,也可能让本来短命的执行状态变成长时间可访问对象。
Runtime state 是比单个 frame 更外层的执行环境。Python 3.14 语言参考中的 runtime model 把运行时概念分成 global runtime state、interpreter state、thread state 等层级。对本章来说,只需要先建立一个可用模型:一个线程执行 Python 代码时,当前 thread state 持有当前调用栈和异常状态;调用栈上的每个 frame 对应一次 code block 执行;frame 通过 globals、locals、builtins 和 closure 连接可见对象。
这个模型可以整理成本章的可复用检查顺序。看到任意 Python 执行现象,先确认当前源码属于哪个 code block;再确认这个 block 的 local、global 和 builtins;再判断是否存在 enclosing function scope;再看当前语句是否创建新的 frame;再沿 call stack 判断返回、异常或暂停位置;如果涉及调试、traceback、生成器或协程,再检查 frame 是否被额外对象持有。
本章到这里建立的是执行模型的骨架。后续章节会分别放大 name binding、object reference、evaluation order、control flow、exception、context manager、bytecode、frame object 和 CPython eval loop。本章提供的价值是让这些后续主题都有同一个定位坐标:源码按 code block 编译和执行,执行状态落在 frame,名字通过 namespace 解析,控制权沿 call stack 推进。
最小自检任务
阅读下面的代码,不运行它,判断 Box.value 的值来自哪里,并说明 x、len、build、run 分别在哪个 namespace 或 frame 关系中解析。
x = "global"
def build():
x = "enclosing"
def run():
return len(x)
return run
class Box:
x = "class"
value = build()()
答案要点
Box.value 的值是 9,因为 build()() 先调用 module namespace 中的 build,再调用它返回的 run。执行 build 时,x = "enclosing" 在 build 的 function frame 中建立局部绑定;run 读取 x 时使用闭包捕获到的 enclosing 变量,所以读取的是字符串 "enclosing"。
class body 中的 x = "class" 写入 class local namespace,之后会成为 Box.x。这个 class namespace 不会成为 run 的 enclosing function scope,因此 run 里的裸名字 x 不读取 Box.x。module 顶层的 x = "global" 也没有被 run 读取,因为 enclosing scope 已经命中。
len 的读取路径是 builtins namespace。run 的 local namespace、build 的 enclosing scope 和 module namespace 都没有绑定 len,所以普通名字查找进入 builtins。这个片段的检查顺序是:确认 class Box 的 body 立即执行;确认 build 来自 module namespace;确认 run 是函数调用产生的 frame;确认 x 先命中 enclosing;确认 len 落到 builtins。
本章知识点总结
- code block:Python 按 code block 组织编译和执行,module、function body、class body、交互命令、
eval()和exec()输入都可以成为执行单元。 - 执行上下文:execution context 决定当前 frame、locals、globals、builtins、异常状态和执行完成后的控制权返回位置。
- module 执行:module 顶层在加载或导入时顺序执行,顶层绑定会写入 module namespace。
- function 执行:函数定义创建 function object,函数体在调用时创建 frame 并执行。
- class 执行:class body 在执行到
classstatement 时立即运行,生成的 namespace 会成为 class object 的属性来源。 - 动态执行:
eval()执行表达式并返回结果,exec()执行语句块并按给定 globals 和 locals 读取或写入名字。 - frame 状态:frame 保存一次 code block 执行中的 code object、locals、globals、builtins、调用来源和异常连接点。
- 名字解析:function block 中普通名字通常按 local、enclosing、global、builtins 的顺序解析。
- class 边界:class namespace 会形成类属性来源,但普通方法体不会把 class namespace 当作 enclosing function scope。
- builtins 层:builtins namespace 是普通名字查找的兜底层,内置对象可能被 local 或 global 绑定遮蔽。
- 调用栈:call stack 记录尚未完成的 frame 嵌套,函数调用、返回、异常传播和暂停恢复都围绕它推进。
- 生命周期:frame 通常随调用结束退出调用栈,但 traceback、生成器、协程和调试工具可以继续持有它。
- 判断顺序:分析执行现象时,先定位 code block,再看 namespace,再看 frame 创建,再沿 call stack 判断返回、异常或暂停路径。