作用域决定了你在代码的哪个位置能访问哪个变量。Python 的作用域规则清晰、一致,掌握它之后,写函数、嵌套函数时就不容易踩坑。闭包则是作用域机制的自然延伸,能让你写出更灵活的函数封装。
LEGB 查找规则
当你在函数内访问一个变量时,Python 会按照 L → E → G → B 的顺序逐级查找:
| 层级 | 英文 | 含义 |
|------|------|------|
| L | Local | 当前函数内部定义的变量 |
| E | Enclosing | 外层函数(如果存在)的局部作用域 |
| G | Global | 模块级别(文件顶层)定义的变量 |
| B | Built-in | Python 内置的名字空间(如 print、len) |
示例:
x = "global" # G
def outer():
x = "enclosing" # E
def inner():
x = "local" # L
print(x) # 先在 L 找到,输出 "local"
inner()
outer()
这里 inner 内部的 x 在 L 层就找到了,所以不会继续向上查 E 或 G。
如果把 inner 里的 x = "local" 删除,那么它会找到 E 层的 "enclosing";如果 outer 里也没有,最终才会到 G 层找到 "global"。
注意: Python 是词法作用域,函数的作用域在定义时就已确定,而不是在运行时动态改变。简单说,你在哪里定义了函数,它就绑定了哪里的嵌套关系。
global 关键字:修改全局变量
在函数内部,直接给变量赋值会被认为是创建了一个新的局部变量,而不是修改全局变量。如果你想在函数里修改全局变量的值,需要用 global 声明。
count = 0
def increment():
global count # 声明 count 是全局的那个
count += 1
increment()
print(count) # 输出 1
如果没有 global count,count += 1 会先尝试读取局部变量 count(还没定义),导致 UnboundLocalError。global 就是告诉解释器:“这个变量来自全局,不要把它当局部变量处理。”
一般在函数中对全局变量只读时不需要声明,只有赋值才会触发这个需求。实际编码中,应尽量少用全局变量;如果一定要用,global 可以使意图更明确。
nonlocal 关键字:修改外层函数的变量
在一个嵌套函数里,如果你想修改外层函数(Enclosing)的变量,而不能也不想用 global,就用 nonlocal。
def outer():
total = 0
def adder(n):
nonlocal total # 声明 total 是外层函数的变量
total += n
return total
return adder
acc = outer()
print(acc(3)) # 3
print(acc(5)) # 8
这里 adder 里 total += n 需要赋值,Python 默认又会在局部创建一个新局部变量,从而遮蔽外层 total。加上 nonlocal total 后,解释器就会操作外层作用域的 total,这才实现了累加效果。
闭包的形成条件
闭包是能够记住定义时所在词法作用域的函数。它必须满足三个条件:
- 嵌套函数:一个函数内部定义了另一个函数。
- 内部函数引用了外部函数的变量(非全局变量)。
- 外部函数将内部函数作为返回值(或传递给其他函数)。
一句话:内部函数抓住了外部函数的变量,然后外部函数把内部函数“扔”了出来。
上面 outer/adder 的例子就是一个典型的闭包:adder 是嵌套函数,它引用了 outer 里的 total,然后 outer 把 adder 返回。当我们后续调用 acc(3) 时,total 并没有因为 outer 执行完毕而消失,它被“封闭”在了 adder 里面。
变量保存机制
外部函数变量的值会存储在内部函数的 closure 属性里(一个元组,每个元素是一个 cell 对象)。这保证了闭包在被外部函数调用结束后依然能访问这些变量。
实用示例:
def make_multiplier(factor):
def multiply(x):
return x * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
double 记住了 factor=2,triple 记住了 factor=3,它们各自持有独立的闭包环境。
常见坑点与最佳实践
- 变量是引用,不是复制:闭包捕获的是变量本身(或说变量的引用),而不是当时的值。如果在循环中创建闭包,且变量在循环中不断变化,可能出现“所有闭包引用的是同一个变量最后一个值”的问题。
funcs = []
for i in range(3):
def f():
return i
funcs.append(f)
print([f() for f in funcs]) # 输出 [2, 2, 2] 而不是 [0, 1, 2]
解决办法是捕获当前值作为默认参数(def f(i=i):)或使用工厂函数立即绑定。
- 不可变类型与赋值:如果闭包内捕获的变量是不可变类型(如数字、字符串),想要“修改”它实际上是在进行重新赋值,这时必须在内部函数里用
nonlocal声明,否则只会创建一个新的局部变量。
闭包让函数有了“记忆”,在工厂函数、装饰器、回调注册等场景中非常实用。理解了作用域与闭包,你对 Python 的变量访问规则就有了一张完整的“查表地图”。