一段代码里 x = 5 的这个 x 到底”在哪里”?跨函数写代码后,你会开始遇到 “为什么 x 在这里能读、那里改不了?” 的疑问。答案就在作用域(scope)——Python 定位变量名的规则。理解 LEGB 规则和闭包,你才能真正驾驭函数、装饰器、生成器这些高级特性。这一篇讲透作用域、global 与 nonlocal、闭包与”闭包变量陷阱”。
一、LEGB 规则
Python 找一个变量名,按四层依次查找:
- L (Local):当前函数的局部;
- E (Enclosing):外层函数(如果嵌套定义)的局部;
- G (Global):模块顶层;
- B (Built-in):Python 内置命名空间。
找不到就 NameError。
1 | x = "global" |
内置层次的例子:
1 | print("hello") # print 在 built-in 里 |
如果你 def print(...) 定义了自己的 print,就会遮蔽内置。所以别用 list、dict、sum 这些做变量名。
二、局部作用域
函数内部赋值的变量默认是局部:
1 | x = 10 |
规则:只要在函数体里有 x = ... 这种赋值语句,x 在整个函数里都被视为局部。哪怕这行在很下面:
1 | x = 10 |
要么全部使用外部 x(不赋值),要么显式声明 global。
三、global:修改全局变量
在函数里想改全局变量,用 global:
1 | count = 0 |
别滥用 global——它会让代码难测、难维护。多数情况改成”返回值 + 调用方赋值”更好:
1 | def next_count(current): |
四、nonlocal:修改外层函数变量
闭包(下面详细讲)常常需要修改外层函数的变量,但不是全局的,这时用 nonlocal:
1 | def counter(): |
如果不写 nonlocal,n += 1 会被当成”新建局部 n”,导致 UnboundLocalError。
nonlocal 与 global 的区别:
global找到模块顶层的变量;nonlocal找到最近的外层函数的变量。
五、什么是闭包
闭包(closure)= “函数 + 它捕获的外层变量“。当外层函数返回内层函数时,内层函数记住了它的定义环境。
1 | def make_multiplier(factor): |
double是一个函数,它”记住”了factor=2;triple是同一份代码,但”记住”的factor=3;- 两者互不影响。
六、闭包能干什么
1. 参数化的函数
1 | def make_greeter(greeting): |
2. 装饰器的底层机制
装饰器本质就是”接受函数,返回函数”,天然就是闭包:
1 | def timed(func): |
wrapper 捕获了 func,就是闭包。
3. 保存状态(比类轻量)
1 | def make_counter(): |
七、闭包变量的可见性
闭包捕获变量本身(更准确说是”cell”),不是那一刻的值。所以外层变量后来变,闭包看到的也变:
1 | def outer(): |
inner 捕获的是 x 这个引用,而不是数值 10。
八、”晚绑定”陷阱:循环里的闭包
这是 Python 最经典的坑之一:
1 | funcs = [] |
原因:所有 lambda 捕获同一个变量 i,循环结束时 i 是 2,所以调用时全部返回 2。
修复 1:默认参数(快照当前值)
1 | funcs = [] |
修复 2:立即调用工厂函数
1 | funcs = [] |
修复 3:偏函数 functools.partial
1 | from functools import partial |
这个坑几乎所有教程都会警告——记住它。
九、检查闭包变量
闭包的”记忆”存在 __closure__ 属性里:
1 | def make(x): |
一般写业务代码不需要用它,但了解原理有助于理解装饰器等高级机制。
十、模块作用域 vs 类作用域
模块顶层的变量属于模块作用域(G 层)。
类体里的变量是个特殊存在——它属于”类的命名空间”,但方法内部访问不到(不属于 LEGB 的 E 层):
1 | class Foo: |
初学者遇到这个会很困惑。规则:类体不是外层作用域,方法内部只能通过 self 或 Foo.x 访问类属性。
十一、变量的生命周期
- 局部变量:函数调用时创建,返回时销毁;
- 闭包捕获的变量:只要闭包函数还存在,就活着;
- 全局变量:程序运行期间一直活;
- 循环变量:Python 里 for 变量会泄漏到外层:
1 | for x in range(3): |
推导式的循环变量不泄漏(3.x),但普通 for 会。
十二、检查名字:locals / globals
1 | def f(): |
调试时偶尔用一下,业务代码不要动 locals() 的返回值(有些实现里改它没效果)。
十三、一个综合例子:简单缓存
用闭包实现一个”最多缓存 N 个结果”的装饰器:
1 | # cache.py |
输出:
1 | 正在计算 1 的平方 |
这个例子融合了闭包、可变默认对象、装饰器概念,本质就是一个 mini 版的 functools.lru_cache。
十四、常见陷阱
- 函数里给全局变量赋值忘了
global:x = ...会新建局部。 - 循环里的闭包晚绑定:改成默认参数或立即工厂。
- UnboundLocalError:函数里既读又赋值同一个名字,全成了局部。
nonlocal找不到目标:外层必须已有这个名字,不能凭空创建。- 类作用域不参与 LEGB:方法内直接写类属性名会 NameError。
- for 变量泄漏:
for x in range(3): pass; print(x)会输出 2。
十五、小结与延伸阅读
- LEGB:Local → Enclosing → Global → Built-in;
global修改全局,nonlocal修改外层函数变量;- 闭包 = 函数 + 它记住的外层变量;
- 闭包记住的是变量的引用,不是快照;
- 循环里创建闭包要小心晚绑定;
- 尽量少用 global,闭包比 global 更”局部化”;
- 类作用域不参与 LEGB。
延伸阅读:
- Python 作用域官方文档:https://docs.python.org/zh-cn/3/tutorial/classes.html#python-scopes-and-namespaces
- Fluent Python 第 7 章:函数装饰器与闭包。
下一篇 匿名函数 lambda 与高阶函数 我们讲 Python 函数式编程的两个主角。