5 minutes
作用域、闭包与命名空间
上一章我们学会了写函数。但你有没有想过:函数里面定义的变量为什么外面访问不到?为什么嵌套函数能"记住"外层函数的变量?这些问题的答案都藏在 Python 的作用域(Scope)和闭包(Closure)机制里。
理解作用域和闭包,是写出高质量 Python 代码的关键一步——它们决定了变量在哪里可见、函数的生命周期如何运作、以及如何优雅地实现状态保持。
命名空间(Namespace)
命名空间是"名字到对象"的映射。当你写 x = 1,Python 就把名字 x 和对象 1 关联起来,这个关联关系就存在于当前命名空间中。
Python 的命名空间分为几种:
# 内置命名空间 —— print, len, int 等内置函数
# 全局命名空间 —— 模块级别的名字
# 局部命名空间 —— 函数内的名字
不同命名空间是独立隔离的——同一个名字在不同空间中可以指向不同的对象。
LEGB 规则
Python 在查找变量时,按照 LEGB 顺序搜索四个作用域:
- Local —— 当前函数内部的局部作用域
- Enclosing —— 外层函数的局部作用域(嵌套函数中)
- Global —— 模块的全局作用域
- Built-in —— Python 内置作用域(
print、len等)
# Built-in 示例 —— 可以直接用内置函数
print(len("hello")) # len 在 Built-in 作用域
# Global 示例 —— 模块级别的变量
x = "全局变量"
def outer():
# Enclosing —— 外围函数作用域
y = "外层变量"
def inner():
# Local —— 局部作用域
z = "内层变量"
# LEGB 查找顺序:先找 z(Local),再找 y(Enclosing),再找 x(Global),最后找内置
print(z) # Local
print(y) # Enclosing
print(x) # Global
inner()
outer()
图解 LEGB
# 全局作用域(Global)
g = 100
def outer_func():
# 外围作用域(Enclosing)
e = 200
def inner_func():
# 局部作用域(Local)
l = 300
# 查找顺序:
print(l) # 1. Local → 找到 300
print(e) # 2. Enclosing 中找到 200
print(g) # 3. Global 中找到 100
print(str) # 4. Built-in 中找到 <class 'str'>
inner_func()
outer_func()
作用域的"墙壁"
函数参数和函数内部赋值的变量都在局部作用域中,外部无法访问:
def my_func(a):
b = 10 # a 和 b 都在局部作用域
return a + b
print(my_func(5)) # 15
# print(a) # NameError: name 'a' is not defined
# print(b) # NameError: name 'b' is not defined
global 关键字
默认情况下,在函数内部对变量赋值是在局部作用域中创建新变量,而不是修改全局变量:
x = 10
def modify():
x = 20 # 这不是修改全局 x,而是创建了一个新的局部 x
print(f"局部 x = {x}")
modify() # 局部 x = 20
print(x) # 10 —— 全局 x 没有被修改
如果你真的想在函数内部修改全局变量,使用 global 关键字声明:
counter = 0
def increment():
global counter # 告诉 Python:我要用全局的 counter
counter += 1
print(f"计数: {counter}")
increment() # 计数: 1
increment() # 计数: 2
increment() # 计数: 3
print(counter) # 3 —— 全局变量确实被修改了
⚠️ 谨慎使用 global
global 会让函数的副作用变得隐式,增加调试难度。通常你可以在设计层面避免它:
# 不好的设计 —— 依赖全局变量
total = 0
def add_to_total(x):
global total
total += x
return total
# 更好的设计 —— 通过参数和返回值
def add(value, x):
return value + x
total = add(total, 5) # 显式更新
nonlocal 关键字
nonlocal 用于嵌套函数中,声明一个变量来自外层(Enclosing)作用域,但不是全局的:
def outer():
count = 0
def inner():
nonlocal count # 说:我要修改外层函数的 count
count += 1
return count
return inner
counter = outer()
print(counter()) # 1
print(counter()) # 2
print(counter()) # 3
如果没有 nonlocal,内层函数的 count += 1 会创建一个新的局部 count,而不是修改外层那个:
def outer():
count = 0
def inner():
# 没有 nonlocal 声明
count += 1 # 错误!因为 count += 1 读取了局部 count(未赋值先引用)
return count
return inner
# counter = outer() # UnboundLocalError: ...
global 和 nonlocal 的区别:
| 关键字 | 作用域层级 | 适用场景 |
|---|---|---|
global |
全局(模块级别) | 在函数中修改模块级变量 |
nonlocal |
外层(Enclosing) | 在嵌套函数中修改外层函数变量 |
变量遮蔽(Variable Shadowing)
当内层作用域的变量与外层作用域同名时,内层的名字"遮蔽"(shadow)了外层名字:
value = 100 # 全局
def show():
value = 200 # 局部——遮蔽了全局的 value
print(f"局部: {value}")
show() # 局部: 200
print(f"全局: {value}") # 全局: 100
遮蔽可能造成迷惑性 bug,尤其是在参数命名时:
# 容易造成迷惑的例子
def calculate(list): # 参数名 list 遮蔽了内置类型 list
list = [x * 2 for x in list] # 这里的 list 不再是 list() 函数
return list
data = [1, 2, 3]
result = calculate(data)
print(result) # [2, 4, 6]
# print(list("abc")) # 在 calculate 外部不受影响
最佳实践: 避免使用 Python 内置名称(list、dict、str、len 等)作为变量名。
闭包(Closure)
闭包是 Python 最优雅的特性之一。当一个嵌套函数引用了外层函数的作用域变量,并且外层函数返回了这个嵌套函数,就形成了一个闭包。
简单说:闭包 = 函数 + 创建时的环境。
创建闭包
创建闭包需要满足三个条件:
- 有嵌套函数
- 内层函数引用了外层函数的变量
- 外层函数返回内层函数(而不是调用它)
def make_multiplier(factor):
"""创建一个将数字乘以 factor 的函数"""
def multiplier(number):
return number * factor
return multiplier # 返回函数对象,不调用
# 创建两个闭包
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 15
这里的关键是 double 和 triple 都"记住"了各自的 factor 值,即使 make_multiplier 早已执行完毕。
检测闭包 —— __closure__ 属性
每个闭包函数都有一个 __closure__ 属性,它是一个包含**自由变量(free variable)**的元组:
def make_greeter(greeting):
def greet(name):
return f"{greeting}, {name}!"
return greet
hello_greet = make_greeter("Hello")
hi_greet = make_greeter("Hi")
print(hello_greet("Alice")) # Hello, Alice!
print(hi_greet("Bob")) # Hi, Bob!
# 查看闭包信息
print(hello_greet.__closure__) # (<cell at ...: str object at ...>,)
print(hello_greet.__closure__[0].cell_contents) # Hello
print(hi_greet.__closure__[0].cell_contents) # Hi
print(hello_greet.__name__) # greet
闭包的常见应用场景
场景 1:计数器
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
# 独立的计数器
c1 = make_counter()
c2 = make_counter()
print(c1()) # 1
print(c1()) # 2
print(c1()) # 3
print(c2()) # 1 —— 独立的计数
print(c2()) # 2
场景 2:带配置的工厂函数
def create_formatter(prefix, suffix):
"""创建格式化器——生成带前后缀的字符串"""
def format(value):
return f"{prefix}{value}{suffix}"
return format
# 创建不同的格式化器
parens = create_formatter("(", ")")
brackets = create_formatter("[", "]")
quotes = create_formatter("'", "'")
print(parens("hello")) # (hello)
print(brackets("world")) # [world]
print(quotes("python")) # 'python'
场景 3:延迟计算
def lazy_power(exponent):
"""延迟计算幂运算"""
def power(base):
return base ** exponent
return power
square = lazy_power(2)
cube = lazy_power(3)
# 在需要的时候才计算
print(square(5)) # 25
print(cube(5)) # 125
场景 4:带缓存的函数工厂
def make_cached_calculator():
"""创建一个带缓存的函数"""
cache = {}
def calculate(n):
if n not in cache:
print(f"正在计算 {n}...")
# 模拟耗时计算
result = sum(range(1, n + 1))
cache[n] = result
return cache[n]
return calculate
calc = make_cached_calculator()
print(calc(100)) # 正在计算 100...\n5050
print(calc(100)) # 5050(来自缓存,没有打印"正在计算")
print(calc(50)) # 正在计算 50...\n1275
闭包 vs 全局变量
同样的功能,可以用全局变量实现,但闭包更优雅:
# 使用全局变量(不推荐)
count_global = 0
def increment_global():
global count_global
count_global += 1
return count_global
# 使用闭包(推荐)
def make_increment():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
inc = make_increment()
| 对比项 | 全局变量方式 | 闭包方式 |
|---|---|---|
| 封装性 | 差——全局变量可以被任何代码修改 | 好——数据对外不可见 |
| 命名污染 | 污染全局命名空间 | 不污染 |
| 多实例 | 只能有一个 | 每个闭包独立 |
| 可读性 | 函数有隐式外部依赖 | 函数是自包含的 |
常见陷阱
陷阱 1:循环中的闭包延迟绑定
这是 Python 中最经典的闭包陷阱:
# 期望:创建 5 个函数,分别返回 0, 1, 2, 3, 4
funcs = []
for i in range(5):
funcs.append(lambda: i)
# 但实际:
for func in funcs:
print(func(), end=" ") # 4 4 4 4 4 —— 全是 4!
原因:闭包中的自由变量 i 只有在函数被调用时才去查找,而不是在函数被定义时。循环结束时 i=4,所以所有函数返回的都是 4。
解决方案 1:使用默认参数(立即绑定)
funcs = []
for i in range(5):
funcs.append(lambda x=i: x) # i 作为默认参数,定义时就被求值
for func in funcs:
print(func(), end=" ") # 0 1 2 3 4
解决方案 2:使用闭包工厂
def make_func(x):
return lambda: x
funcs = [make_func(i) for i in range(5)]
for func in funcs:
print(func(), end=" ") # 0 1 2 3 4
陷阱 2:误以为闭包是"拷贝"变量
def make_adder(x):
def add(y):
return x + y
return add
add5 = make_adder(5)
print(add5(3)) # 8
# 闭包不是快照——如果 x 是可变的,变化会反映
def make_accumulator():
items = []
def add(item):
items.append(item) # 修改的是外部的 items 列表
return items
return add
acc = make_accumulator()
print(acc(1)) # [1]
print(acc(2)) # [1, 2]
print(acc(3)) # [1, 2, 3]
实战:使用闭包构建简易状态机
def make_traffic_light():
"""创建一个交通灯状态机"""
states = ["红灯", "绿灯", "黄灯"]
index = 0
def next_light():
nonlocal index
current = states[index]
index = (index + 1) % len(states)
return current
def current_light():
return states[index]
return next_light, current_light
next_light, current_light = make_traffic_light()
print(current_light()) # 红灯
print(next_light()) # 红灯 → 绿灯
print(next_light()) # 绿灯 → 黄灯
print(next_light()) # 黄灯 → 红灯
print(current_light()) # 红灯
这个模式本质上是将状态封装在闭包中,只通过返回的函数来操作状态——这就是封装的一种实现。
小结
本章我们深入探讨了 Python 的作用域和闭包机制:
- LEGB 规则:变量查找按 Local → Enclosing → Global → Built-in 顺序
global:在函数内部声明要修改全局变量nonlocal:在嵌套函数中声明要修改外层函数的变量- 变量遮蔽:内层作用域的同名变量会隐藏外层变量
- 闭包:嵌套函数 + 外层函数的变量环境 = 能"记住"状态的函数
__closure__属性可以查看闭包捕获的自由变量- 闭包的典型应用:计数器、工厂函数、延迟计算、缓存
- 循环闭包陷阱:延迟绑定导致所有函数共享同一个变量值
下一步: 掌握了函数和作用域之后,我们来探索函数式编程的利器——高阶函数和 Lambda 表达式。
Summary: LEGB 作用域规则、闭包机制与常见陷阱。