作用域与命名空间
Python 中变量的可见性和生命周期由命名空间(namespace)和作用域(scope)控制。命名空间是名称到对象的映射,作用域是 Python 代码可以直接访问命名空间的文本区域。理解 LEGB 规则和闭包机制,是掌握 Python 函数高级用法的基础。
命名空间的种类
Python 程序执行期间存在多种命名空间:
- 局部命名空间:函数调用时创建,包含函数的局部变量。函数返回或抛出异常时销毁。
- 全局命名空间:模块级别,包含模块的全局变量、函数定义、类定义等。在模块文件执行时创建,持续到解释器退出。
- 内置命名空间:包含内置函数(如
len、print)和内置异常。Python 启动时创建,永不销毁。
这些命名空间彼此独立,相同名称在不同命名空间中互不干扰:
len = 10 # 全局命名空间中创建 len,遮蔽内置的 len
def demo():
len = "局部" # 局部命名空间中创建 len,遮蔽全局的 len
print(len) # 局部
demo()
print(len) # 10(全局)
print(__builtins__.len("abc")) # 3,通过 __builtins__ 仍可访问内置 len
LEGB 规则
Python 按 LEGB 顺序解析名称:
- Local:当前函数的局部命名空间
- Enclosing:外层(嵌套)函数的局部命名空间(从内到外逐层查找)
- Global:当前模块的全局命名空间
- Built-in:内置命名空间
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x) # local —— 先查局部
inner()
print(x) # enclosing —— outer 的局部
outer()
print(x) # global
如果某一层找不到名称,就继续向上一层查找,直到 Built-in。如果 Built-in 也找不到,抛出 NameError。
赋值与作用域的关系
赋值操作决定了一个名称属于哪个作用域。如果在函数内部对变量赋值,Python 默认认为该变量是局部变量,即使外层有同名变量:
x = "global"
def demo():
print(x) # 这里会报错!
x = "local"
demo()
# UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
为什么会报错?因为 Python 在编译函数体时,看到 x = "local" 就把 x 标记为局部变量。执行到 print(x) 时,解释器在局部命名空间中查找 x,但此时还未赋值,所以抛出 UnboundLocalError。
如果确实需要修改全局变量,必须使用 global 声明;如果需要修改外层函数变量,必须使用 nonlocal 声明。这两个关键字详见《global 与 nonlocal》文档。
闭包
闭包(closure)是指嵌套函数引用了外层函数的变量,并且外层函数已经返回,但内层函数仍然可以访问这些变量的机制。闭包让函数"记住"它被创建时的环境:
def make_power(exponent):
def power(base):
return base ** exponent # 引用外层变量 exponent
return power
square = make_power(2)
cube = make_power(3)
print(square(5)) # 25
print(cube(5)) # 125
这里 square 和 cube 都是闭包。它们各自记住了创建时的 exponent 值(2 和 3),即使 make_power 已经执行完毕。
闭包的关键在于自由变量(free variable):被内层函数引用、但不在内层函数局部命名空间中定义的名称。Python 把自由变量保存在函数对象的 __closure__ 属性中:
print(square.__closure__) # (<cell at ...: int object at ...>,)
print(square.__closure__[0].cell_contents) # 2
闭包与可变对象
闭包捕获的是变量引用,不是值的副本。如果自由变量是可变对象,闭包可以观察到外部对它的修改:
def make_accumulator():
total = [0] # 用列表包装,使其可变
def add(value):
total[0] += value # 修改列表内容,不是重新绑定
return total[0]
return add
acc = make_accumulator()
print(acc(10)) # 10
print(acc(5)) # 15
print(acc(3)) # 18
注意这里不能写 total += value(等价于 total = total + value,会重新绑定变量),因为闭包中的自由变量默认是只读的,重新绑定需要 nonlocal。
延迟绑定陷阱
闭包中的自由变量按名称查找,不是按值捕获。这在循环中创建多个闭包时会导致意外行为:
def make_multipliers():
return [lambda x: i * x for i in range(3)]
multipliers = make_multipliers()
print(multipliers[0](2)) # 4,不是 0!
print(multipliers[1](2)) # 4,不是 2!
print(multipliers[2](2)) # 4
所有闭包共享同一个 i,而 i 在循环结束后是 2。修复方法是利用默认参数在定义时求值的特性:
def make_multipliers_fixed():
return [lambda x, i=i: i * x for i in range(3)]
multipliers = make_multipliers_fixed()
print(multipliers[0](2)) # 0
print(multipliers[1](2)) # 2
print(multipliers[2](2)) # 4
常见错误
误以为函数可以修改全局变量而不声明:
count = 0
def increment():
count += 1 # UnboundLocalError
return count
在循环中创建闭包时忽视延迟绑定:
handlers = []
for i in range(3):
handlers.append(lambda: print(i))
for h in handlers:
h() # 全部打印 2
小结
Python 按 LEGB 顺序解析名称:局部、外层、全局、内置。函数内赋值默认创建局部变量,可能遮蔽外层同名变量。闭包让内层函数记住外层环境,是函数式编程和装饰器的核心机制。理解作用域规则和闭包陷阱,是写出可靠嵌套函数的关键。