Skip to main content

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_namemakersnapshot 这些名字。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。贯穿代码的 nameouterDemoresult 都属于这个 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,也携带对外层变量 labelstatus 的闭包关系。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 中的 outersnapshot = 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_boxlocal_box,所以 answer 写入 local_boxlen(name)name 会按动态执行时的名字解析规则读取,name 来自 global_boxlen 来自 builtins namespace。随后 eval() 读取同一组 namespace,answerlocal_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_codef_localsf_globalsf_builtinsf_backf_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 能看到 valueframe 以及闭包变量的当前值;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。这个顺序不是装饰性口诀,它决定 labelstatusnamelen 在贯穿代码中分别来自哪里。

inner(value) 内部,value 是 local,因为它来自当前函数的形参绑定。labelstatus 是 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_nameself.class_name 或描述符路径读取,方法体里的裸名字保留 function block 的名字解析规则。

当你排查 NameErrorUnboundLocalError 或名字被覆盖的问题时,先确定当前语句属于哪个 code block,再找这个 block 中是否存在绑定操作,再沿对应 namespace 顺序读取。UnboundLocalError 的典型原因是:同一 function block 中存在对某名字的绑定,编译阶段把它归为 local,而读取发生在该 local 值建立之前。

1.6 builtins namespace 的最终查找层

builtins namespace 是普通名字查找的兜底层。lenopenExceptionobjectrange 等内置对象通过这一层进入 code block。它的位置很靠后:local、enclosing 和 global 都没有命中时,运行时才会查 builtins。

贯穿代码中的 len([value]) 展示了这个层级。inner 的 local namespace 没有 lenouter 的闭包变量里也没有 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,outerinner 的 frame 都会短暂位于 class body frame 之上。等 class body 完成后,运行时才用收集到的 namespace 生成 Demo

H → I 展示 module 顶层后半段的调用链。outer("from-module-body") 返回 inner,紧接着 (3) 调用这个返回值。执行 inner 时,当前 frame 可以读取自己的 local、闭包里的 labelstatus、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 链被异常对象间接持有。生成器和协程会在 yieldawait 附近暂停,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 的值来自哪里,并说明 xlenbuildrun 分别在哪个 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 在执行到 class statement 时立即运行,生成的 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 判断返回、异常或暂停路径。