名字查找遵循 LEGB 四层规则;闭包 = 函数 + 捕获的自由变量,是理解装饰器与延迟绑定陷阱的基础。
LEGB 查找规则
解释器解析一个名字时,按 Local → Enclosing → Global → Built-in 顺序查找,命中即停:
x = "global" # Global
def outer():
x = "enclosing" # Enclosing
def inner():
x = "local" # Local
print(x) # local
inner()
outer()
print(len) # Built-in:内建名字- Local:当前函数体内的赋值(含参数)即产生局部名字,编译期确定
- Enclosing:外层函数作用域,只有嵌套函数才存在
- Global:模块级名字
- Built-in:
len、Exception等内建名字
注意:函数内只要出现赋值,名字就在编译期被标记为局部——哪怕赋值写在引用之后,引用处也会抛 UnboundLocalError。
global 与 nonlocal
默认函数内赋值只创建局部名字;改写外层名字必须显式声明:
count = 0
def inc():
global count # 绑定到模块级 count
count += 1
def make_counter():
n = 0
def counter():
nonlocal n # 绑定到外层函数的 n
n += 1
return n
return counterglobal/nonlocal 是声明而非定义:它们让赋值指向既有的外层名字。滥用 全局变量 造成的状态隐蔽耦合,根源常在这里。
闭包
闭包(closure)= 函数对象 + 它引用的外层自由变量。内层函数即使被返回到外层之外,自由变量仍被 cell 对象持有,不随外层调用结束而销毁:
def multiplier(factor):
def multiply(x):
return x * factor # factor 是自由变量
return multiply
double = multiplier(2)
double(10) # 20
double.__closure__ # (<cell at ...: int object at ...>,)
double.__closure__[0].cell_contents # 2判断标准:内层函数引用了外层变量,且逃逸到定义作用域之外。闭包提供轻量的状态封装——保存一个状态不必定义类。
延迟绑定坑
闭包捕获的是变量的引用,而非定义那一刻的值。循环里创建的 lambda 共享同一个变量,调用时才求值:
funcs = [lambda: i for i in range(3)]
[f() for f in funcs] # [2, 2, 2] —— 全部读到循环结束后的 i
# 修复:默认参数在定义时求值,固化当前值
funcs = [lambda i=i: i for i in range(3)]
[f() for f in funcs] # [0, 1, 2]闭包与装饰器
装饰器是闭包最典型的应用:wrapper 捕获被装饰的 func,两者绑定存活。理解 Python装饰器 中三层嵌套函数的先决条件,就是本篇的作用域与闭包机制。