名字查找遵循 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 counter

global/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装饰器 中三层嵌套函数的先决条件,就是本篇的作用域与闭包机制。