Python 作用域与闭包机制

LEGB 规则

Python 查找变量时遵循四层作用域链:

层级 名称 说明 示例
L Local 当前函数内部的变量 函数形参或局部定义
E Enclosing 外层函数中的变量 闭包中的自由变量
G Global 当前模块的全局变量 脚本或模块顶层定义
B Built-in Python 内建命名空间 len, print, range

✅ 查找顺序:Local → Enclosing → Global → Built-in

UnboundLocalError 的根源

当 Python 发现函数中有对变量的赋值时,它会在编译阶段将该变量标记为“局部变量”。

1
2
def inner():
x = x + 1 # 报错:局部变量 x 在赋值前被引用

简单来说,Python 在编译函数定义阶段(不是运行时)就会先扫描整个函数体,看哪些变量被赋值(=)。凡是出现过赋值的变量,Python 就认定它是局部变量(Local)

解决方式:

  • 告诉 Python “我想用外层变量”:

    • nonlocal x → 使用外层函数作用域变量
    • global x → 使用全局变量

闭包(Closure)定义

当一个函数嵌套在另一个函数中,且内部函数引用了外层函数的变量
Python 会在内部函数对象中“捕获”那个外层变量的引用——此时形成闭包。

例:

1
2
3
4
5
6
7
8
9
10
11
12
def outer():
count = 0
def inner():
nonlocal count
count += 1
print(count)
return inner

f = outer()
f() # 1
f() # 2
f() # 3

✅ 闭包让外层变量在函数退出后依然存在。

常见错误理解

误区 正解
“每次函数调用都会新建变量” 仅当外层函数再次执行时才新建作用域
nonlocal 等同于 global nonlocal 仅影响外层函数作用域,不触及全局
“闭包保存的是变量值” 实际保存的是变量引用

闭包陷阱:循环变量与 lambda

经典例子:

1
2
3
4
5
6
funcs = []
for i in range(3):
funcs.append(lambda: print(i))

for f in funcs:
f()
  • 输出:
1
2
3
2
2
2
  • 原因:

    • lambda 捕获的是 i 的引用,循环结束时 i = 2
    • 所有 lambda 打印的都是同一个 i 的最终值

解决方法:绑定默认参数

1
2
3
4
5
6
7
8
funcs = []
for i in range(3):
funcs.append(lambda i=i: print(i)) # 捕获当时 i 的值

# 输出:
# 0
# 1
# 2

列表推导也适用:

1
2
3
4
5
6
7
8
9
funcs = [lambda x=i: x*2 for i in range(4)]
for f in funcs:
print(f())

# 输出:
# 0
# 2
# 4
# 6

要点总结

  1. LEGB 规则是理解 Python 变量查找的基础。

  2. nonlocal 与 global

    • nonlocal → 修改外层函数作用域变量
    • global → 修改全局变量
  3. 闭包的核心

    • 捕获的是变量引用而非值
    • 外层变量在函数退出后仍然存在
  4. 循环闭包陷阱

    • 循环变量被 lambda 或函数捕获时,默认捕获引用
    • 解决方法:使用默认参数绑定当前值
  5. 应用场景

    • 闭包可用于状态保持、函数工厂、装饰器实现等