读 Python behind the scenes #5: how variables are implemented in CPython

读 Python behind the scenes #5: how variables are implemented in CPython

💡 原文中文,约6400字,阅读约需16分钟。
📝

内容提要

本文解析CPython中变量赋值的实现原理,通过字节码操作码(如LOAD_NAME、STORE_NAME、LOAD_FAST等)说明变量如何存储和查找,涉及命名空间(locals、globals、builtins)与作用域(LEGB规则)。不同代码块(模块、函数、类)使用不同操作码,如函数局部变量用LOAD_FAST提升性能,闭包用LOAD_DEREF,全局变量用LOAD_GLOBAL。文章强调变量赋值远比表面复杂,为理解Python对象系统铺垫。

🔎

延伸解读

性能差异:STORE_FAST 为何更快

文章通过基准测试展示了 STORE_FAST 比 STORE_NAME 快约 40%(4.5 秒 vs 2.6 秒)。原因在于 STORE_FAST 直接通过静态计算的索引访问 frame 的 f_localsplus 数组,而 STORE_NAME 需要字典查找。这解释了为什么 Python 编译器在函数内部使用 LOAD_FAST/STORE_FAST 来优化局部变量访问,而模块和类体则使用较慢的 LOAD_NAME/STORE_NAME。

作用域规则决定操作码选择

文章中的表格清晰展示了不同代码块(模块、函数、类)如何根据 LEGB 规则选择不同的操作码。例如,函数内局部变量用 LOAD_FAST,闭包变量用 LOAD_DEREF,全局变量用 LOAD_GLOBAL。类体特殊,使用 LOAD_NAME 和 LOAD_CLASSDEREF。理解这些操作码有助于调试 NameError 或 UnboundLocalError,例如在类中访问外部函数变量时需注意作用域。

闭包与 nonlocal 的陷阱

文章通过示例说明,在内部函数中修改外部变量时,必须声明 nonlocal,否则 Python 会将其视为局部变量,导致 UnboundLocalError。这是因为编译器在编译阶段就决定了变量的作用域,而非运行时。闭包变量存储在 cell 对象中,由外部函数的 frame 和内部函数的 __closure__ 共享,理解这一点有助于避免闭包使用中的常见错误。

Q&A

CPython中变量赋值语句 `a = b` 在字节码层面是如何实现的?

在CPython中,`a = b` 会被编译为两条字节码指令:`LOAD_NAME` 和 `STORE_NAME`。`LOAD_NAME` 从命名空间中查找变量 `b` 的值并压入值栈,`STORE_NAME` 从栈中弹出该值并存储到变量 `a` 对应的命名空间中。

CPython中 `LOAD_NAME` 和 `STORE_NAME` 操作码是如何工作的?

`STORE_NAME` 从值栈弹出值,并通过 `f_locals` 字典将变量名与值关联。`LOAD_NAME` 首先在 `f_locals` 中查找变量,若未找到则依次在 `f_globals` 和 `f_builtins` 中查找,若都未找到则抛出 `NameError`。

CPython中为什么函数局部变量使用 `LOAD_FAST` 而不是 `LOAD_NAME`?

因为函数局部变量在编译时就能确定其在 `f_localsplus` 数组中的索引,使用 `LOAD_FAST` 可以直接通过索引访问,避免了字典查找的开销,从而提升性能。

CPython中闭包变量是如何存储和访问的?

闭包变量存储在 `cell` 对象中,外层函数的 `f_localsplus` 和内部函数的 `__closure__` 都引用同一个 `cell`。访问闭包变量使用 `LOAD_DEREF`,修改使用 `STORE_DEREF`。

在CPython中,全局变量和内置变量是如何查找的?

全局变量通过 `LOAD_GLOBAL` 操作码在 `f_globals` 中查找,若未找到则继续在 `f_builtins` 中查找。内置变量(如 `len`、`int`)也通过 `LOAD_GLOBAL` 查找,但最终会回退到 `f_builtins`。

CPython中类体(class body)中的变量赋值使用哪些操作码?

类体中的变量赋值使用 `STORE_NAME` 和 `LOAD_NAME`,因为类体是一个独立的命名空间,但它的作用域规则与函数不同,不能访问闭包变量,除非使用 `LOAD_CLASSDEREF`。

CPython中 `nonlocal` 关键字的作用是什么?如果不使用会怎样?

`nonlocal` 关键字用于在嵌套函数中声明变量属于外层函数,从而允许修改闭包变量。如果不使用 `nonlocal`,在内部函数中对同名变量赋值会被视为局部变量,导致 `UnboundLocalError`。

CPython中命名空间(namespace)和作用域(scope)有什么区别?

命名空间是存储变量名到值的映射的字典,如 `f_locals`、`f_globals`、`f_builtins`。作用域是程序中可以直接访问这些命名空间的文本区域,遵循LEGB规则(Local、Enclosing、Global、Built-in)。

🏷️

标签

➡️

继续阅读