Python全局变量与局部变量的区别及LEGB查找规则
深入解析Python全局变量与局部变量的生命周期与存储差异,详解Local、Enclosing、Global、Built-in四层LEGB作用域查找顺序,剖析赋值遮蔽与UnboundLocalError的成因及规范解决方案。
NameError。真正理解全局与局部变量的隔离机制及静态作用域编译规则,是避开 UnboundLocalError 陷阱、写出安全高效代码的基础。
一、全局变量与局部变量的核心差异
区分全局变量与局部变量,不能仅仅停留在“写在函数外面还是里面”的直观印象,更需要从定义位置、底层存储机制、生命周期以及访问边界四个维度进行本质分析。
1. 全局变量(Global Variable)
凡是在 Python 模块(即 .py 文件)最外层顶格书写、未缩进在任何函数或类内部的变量赋值,均归属于全局变量。在底层,全局变量存储在当前模块的 __dict__ 命名空间中,可以通过内置函数 globals() 直接查看这个字典结构。
全局变量从被赋值的那一刻开始创建,直到整个 Python 脚本执行结束或解释器进程关闭时才被彻底销毁。因此,它的生命周期贯穿程序的整个运行期。本模块内定义的所有函数在默认情况下都可以直接读取该全局变量的值。
2. 局部变量(Local Variable)
定义在函数内部的变量(包括函数的形参以及在函数体中通过等号赋值产生的变量)被称为局部变量。在底层,CPython 虚拟机为提升函数执行效率,并不会为每个函数调用创建完整字典,而是使用快速连续数组(fastlocals)直接绑定在当前函数的调用栈帧(Frame Object)上,可通过 locals() 查看当前快照。
局部变量具有严格的调用期生命周期:函数每一次被调用时都会在调用栈上新建一个栈帧,局部变量随之分配;一旦函数执行完毕并返回结果,栈帧便从调用栈中弹出并销毁,函数内生成的对象引用计数减 1。除非存在外部闭包引用,否则局部变量所占用的内存会由垃圾回收机制立即回收。
| 对比维度 | 全局变量 (Global) | 局部变量 (Local) |
|---|---|---|
| 定义区域 | 模块文件顶层(无缩进) | 函数体内部或形式参数 |
| 底层存储 | 模块命名空间字典 globals() |
函数调用栈帧 f_locals 数组 |
| 创建时机 | 模块首次导入或执行到该语句时 | 函数每次被调用执行时 |
| 销毁时机 | 进程退出、模块卸载时 | 当前函数执行完毕、栈帧弹出时 |
| 作用范围 | 当前模块所有代码块均可读取 | 严格限制在定义它的函数体内部 |
二、LEGB 变量查找规则深度剖析
当 Python 代码中出现一行读取变量的表达式(例如 print(count) 或 result = base * 2)时,解释器如何知道这个 count 或 base 到底对应哪个对象?答案就是 LEGB 查找规则。
LEGB 是四个英文单词首字母的缩写,分别代表 Python 中四种不同层次的作用域。解释器在解析变量名时,严格按照从 L 到 E,再到 G,最后到 B 的单向链路依次检索。如果在某个作用域找到了匹配的标识符,查找流程立即终止并返回该值;如果找遍四个层级均未找到,则抛出 NameError 异常。
1. L(Local):当前局部作用域
这是查找的第一站。如果在函数内部执行代码,解释器首先在当前函数的栈帧局部符号表中寻找该名称。如果变量在此处定义,则直接命中,查找耗时最短。
2. E(Enclosing):嵌套函数的闭包外层作用域
当一个函数内部嵌套了另一个函数(常见于闭包、装饰器),内层函数在局部作用域未找到变量时,会逐层向外检查包裹它的外层函数的局部作用域。如果嵌套有多层,Python 会按照由内向外的顺序逐层回溯,直到最外层的闭包函数。
3. G(Global):当前模块的全局作用域
如果外层函数也没有该变量,或者当前代码本身就处于顶层函数中,查找将转向当前 .py 模块的全局命名空间。需要特别注意的是,这里的“全局”并不是跨文件的全局,而是指当前模块文件之内的全局。
4. B(Built-in):Python 解释器内置作用域
这是兜底的一层作用域。Python 在启动时会自动加载内置模块 builtins,其中包含了所有内置常量、内置函数(如 print、len、range、sum)以及标准异常类(如 ValueError、KeyError)。只有当 L、E、G 三层均未找到时,解释器才会检索 Built-in。
四层作用域同台验证示例
下面这段代码完整演示了从 L、E、G 到 B 的逐层覆盖机制:
# 模块顶层:定义全局变量 (G)
x = "【G 全局变量】"
def outer():
# 闭包外层函数:定义闭包变量 (E)
x = "【E 闭包外层变量】"
def inner():
# 局部函数:定义局部变量 (L)
x = "【L 局部变量】"
print("inner 内部读取:", x) # 命中 L
inner()
print("outer 内部读取:", x) # outer 栈帧中命中自己的 L(相对于 inner 是 E)
outer()
print("模块顶层读取:", x) # 命中 G
print("内置函数调用:", len) # 前三层无 len,命中 B (built-in function len)
执行该代码的输出结果:
inner 内部读取: 【L 局部变量】
outer 内部读取: 【E 闭包外层变量】
模块顶层读取: 【G 全局变量】
内置函数调用: <built-in function len>
list = [1, 2, 3] 或 str = "abc" 的变量,根据 G 优先于 B 的规则,内置的 list() 和 str() 类型构造器就会被你的同名变量遮蔽(Shadowing),导致后续调用 list("test") 触发 TypeError: 'list' object is not callable。
三、常见困惑辨析:读取与赋值的差异及 UnboundLocalError 陷阱
在 Python 初学者的实践中,最常遇到的作用域报错非 UnboundLocalError 莫属。例如以下看似符合直觉的代码:
total = 100 # 全局变量
def add_tax():
print("当前 total 为:", total) # 这里预期输出 100
total = 120 # 修改 total
add_tax()
运行这段代码时,解释器并未输出 100,而是直接崩溃报错:
UnboundLocalError: local variable 'total' referenced before assignment
为什么明明上面定义了全局变量 total = 100,在执行 print(total) 时 Python 却抱怨“局部变量在赋值前被引用”?
1. 静态作用域判定原理(Static Scope Resolution)
许多初学者误以为 Python 是纯动态、逐行解释执行的语言,认为在执行到 print(total) 时它应该先按全局变量读取,等走到下一行 total = 120 时才变成局部变量。这种理解是错误的。
Python 的作用域判定是在编译期(Bytecode Compilation)静态完成的。在函数被调用之前,编译器会通篇扫描该函数体内的所有语句:
- 如果函数体内出现了任何对某个变量名的赋值操作(例如
var = ...、var += ...,或者for var in ...),且没有显式使用global或nonlocal声明,编译器就会在静态符号表中将var全量标记为当前函数的局部变量。 - 一旦标记为局部变量,编译器在生成字节码时,函数内所有读取该变量的地方都会被编译成快速栈帧读取指令
LOAD_FAST,而绝不会走全局查找指令LOAD_GLOBAL。
当函数运行时,执行到第一行 print(total),触发 LOAD_FAST total。此时栈帧中的局部槽位尚未被赋值填充(值仍为 NULL),解释器立即抛出 UnboundLocalError,流程在此中断,根本不会执行到后面的赋值语句。
2. 就地修改(In-Place Mutation)与重新绑定(Re-binding)的区别
与上述情况形成鲜明对比的是可变对象(如列表、字典、集合):
items = [1, 2, 3] # 全局可变对象
def modify_list():
items.append(4) # 正常执行!无需 global 声明
print("函数内:", items)
modify_list()
print("全局结果:", items) # 输出: [1, 2, 3, 4]
为什么 items.append(4) 不需要 global 声明也不会报错?
因为 items.append() 是方法调用(属性访问并就地修改),而不是对变量名 items 进行重新绑定(Re-binding 赋值)。在编译器看来,函数内部只有 LOAD_GLOBAL items 读取操作,没有 items = ... 赋值,因此 items 依然被正确解析为全局变量。
但如果你在函数内写 items = items + [4],因为出现了等号赋值,items 就会被判定为局部变量,从而再次触发 UnboundLocalError。
四、跨作用域修改的最佳实践与选型指南
当确实需要在内部作用域修改外部变量时,Python 提供了 global 和 nonlocal 关键字,但在工程实践中,盲目滥用这些关键字往往是代码设计不良的信号。
1. global 关键字的使用与风险
global 用于在函数内部显式告知编译器:“请将该标识符视为模块全局作用域的变量,不要在当前栈帧为其创建局部变量”。
run_count = 0
def increment():
global run_count # 声明使用全局变量
run_count += 1 # 成功修改全局状态
increment()
print(run_count) # 输出: 1
def increment_pure(count):
return count + 1
run_count = increment_pure(run_count)
2. nonlocal 关键字与闭包状态维护
Python 3 引入了 nonlocal 关键字,专门用于解决嵌套函数修改外层闭包作用域(E 层)变量的问题。与 global 不同,nonlocal 只会向上回溯至外层嵌套函数,绝不会查找到模块全局作用域(G 层)。
def make_counter(start=0):
count = start # 处于 outer 函数的局部作用域 (对于 inner 是 Enclosing)
def counter():
nonlocal count # 显式声明绑定外层闭包变量
count += 1
return count
return counter
c1 = make_counter(10)
print(c1()) # 输出: 11
print(c1()) # 输出: 12
3. 面向对象(Class)封装:多状态共享的成熟选择
当需要维护较多相互关联的状态,或者多个函数需要协同读写这些状态时,定义一个类是将状态与逻辑内聚的最佳实践。通过 self.attribute 访问实例属性,完全避开了全局变量与 nonlocal 的混淆,同时也保证了多个实例之间状态完全隔离。
五、核心规则速查总结清单
为了在日常编码与代码审查中快速定位作用域相关问题,请牢记以下四条黄金准则:
- 查值四部曲(LEGB):遇到标识符读取,依次沿
Local ➜ Enclosing ➜ Global ➜ Built-in单向回溯,直到首次命中或抛出NameError。 - 赋值决作用域:只要函数体内存在未声明
global/nonlocal的赋值操作,该变量在整个函数体内从首行起均被静态判定为局部变量。 - 先读后赋必报错:在未声明
global的函数中,若在赋值语句之前尝试读取该变量,必然触发UnboundLocalError。 - 传递优于共享:编写函数时,优先使用参数传递入参、使用
return返回计算结果;避免跨层修改全局变量,保持函数纯净与解耦。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









