上一章我们学会了写函数。但你有没有想过:函数里面定义的变量为什么外面访问不到?为什么嵌套函数能"记住"外层函数的变量?这些问题的答案都藏在 Python 的作用域(Scope)和闭包(Closure)机制里。

理解作用域和闭包,是写出高质量 Python 代码的关键一步——它们决定了变量在哪里可见、函数的生命周期如何运作、以及如何优雅地实现状态保持。

命名空间(Namespace)

命名空间是"名字到对象"的映射。当你写 x = 1,Python 就把名字 x 和对象 1 关联起来,这个关联关系就存在于当前命名空间中。

Python 的命名空间分为几种:

# 内置命名空间 —— print, len, int 等内置函数
# 全局命名空间 —— 模块级别的名字
# 局部命名空间 —— 函数内的名字

不同命名空间是独立隔离的——同一个名字在不同空间中可以指向不同的对象。

LEGB 规则

Python 在查找变量时,按照 LEGB 顺序搜索四个作用域:

  1. Local —— 当前函数内部的局部作用域
  2. Enclosing —— 外层函数的局部作用域(嵌套函数中)
  3. Global —— 模块的全局作用域
  4. Built-in —— Python 内置作用域(printlen 等)
# 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: ...

globalnonlocal 的区别:

关键字 作用域层级 适用场景
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 内置名称(listdictstrlen 等)作为变量名。

闭包(Closure)

闭包是 Python 最优雅的特性之一。当一个嵌套函数引用了外层函数的作用域变量,并且外层函数返回了这个嵌套函数,就形成了一个闭包。

简单说:闭包 = 函数 + 创建时的环境

创建闭包

创建闭包需要满足三个条件:

  1. 有嵌套函数
  2. 内层函数引用了外层函数的变量
  3. 外层函数返回内层函数(而不是调用它)
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

这里的关键是 doubletriple 都"记住"了各自的 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 作用域规则、闭包机制与常见陷阱。