人人都会AI编程

9.3 作用域与闭包

更新时间:2026-07-12

作用域决定了你在代码的哪个位置能访问哪个变量。Python 的作用域规则清晰、一致,掌握它之后,写函数、嵌套函数时就不容易踩坑。闭包则是作用域机制的自然延伸,能让你写出更灵活的函数封装。

LEGB 查找规则

当你在函数内访问一个变量时,Python 会按照 L → E → G → B 的顺序逐级查找:

| 层级 | 英文 | 含义 |
|------|------|------|
| L | Local | 当前函数内部定义的变量 |
| E | Enclosing | 外层函数(如果存在)的局部作用域 |
| G | Global | 模块级别(文件顶层)定义的变量 |
| B | Built-in | Python 内置的名字空间(如 printlen) |

示例:

x = "global"             # G

def outer():
    x = "enclosing"      # E
    def inner():
        x = "local"      # L
        print(x)         # 先在 L 找到,输出 "local"
    inner()

outer()

这里 inner 内部的 x 在 L 层就找到了,所以不会继续向上查 E 或 G。

如果把 inner 里的 x = "local" 删除,那么它会找到 E 层的 "enclosing";如果 outer 里也没有,最终才会到 G 层找到 "global"

注意: Python 是词法作用域,函数的作用域在定义时就已确定,而不是在运行时动态改变。简单说,你在哪里定义了函数,它就绑定了哪里的嵌套关系。

global 关键字:修改全局变量

在函数内部,直接给变量赋值会被认为是创建了一个新的局部变量,而不是修改全局变量。如果你想在函数里修改全局变量的值,需要用 global 声明。

count = 0

def increment():
    global count          # 声明 count 是全局的那个
    count += 1

increment()
print(count)              # 输出 1

如果没有 global countcount += 1 会先尝试读取局部变量 count(还没定义),导致 UnboundLocalErrorglobal 就是告诉解释器:“这个变量来自全局,不要把它当局部变量处理。”

一般在函数中对全局变量只读时不需要声明,只有赋值才会触发这个需求。实际编码中,应尽量少用全局变量;如果一定要用,global 可以使意图更明确。

nonlocal 关键字:修改外层函数的变量

在一个嵌套函数里,如果你想修改外层函数(Enclosing)的变量,而不能也不想用 global,就用 nonlocal

def outer():
    total = 0
    def adder(n):
        nonlocal total      # 声明 total 是外层函数的变量
        total += n
        return total
    return adder

acc = outer()
print(acc(3))    # 3
print(acc(5))    # 8

这里 addertotal += n 需要赋值,Python 默认又会在局部创建一个新局部变量,从而遮蔽外层 total。加上 nonlocal total 后,解释器就会操作外层作用域的 total,这才实现了累加效果。

闭包的形成条件

闭包是能够记住定义时所在词法作用域的函数。它必须满足三个条件:

  1. 嵌套函数:一个函数内部定义了另一个函数。
  2. 内部函数引用了外部函数的变量(非全局变量)。
  3. 外部函数将内部函数作为返回值(或传递给其他函数)。

一句话:内部函数抓住了外部函数的变量,然后外部函数把内部函数“扔”了出来。

上面 outer/adder 的例子就是一个典型的闭包:adder 是嵌套函数,它引用了 outer 里的 total,然后 outeradder 返回。当我们后续调用 acc(3) 时,total 并没有因为 outer 执行完毕而消失,它被“封闭”在了 adder 里面。

变量保存机制

外部函数变量的值会存储在内部函数的 closure 属性里(一个元组,每个元素是一个 cell 对象)。这保证了闭包在被外部函数调用结束后依然能访问这些变量。

实用示例:

def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)

print(double(5))   # 10
print(triple(5))   # 15

double 记住了 factor=2triple 记住了 factor=3,它们各自持有独立的闭包环境。

常见坑点与最佳实践

  • 变量是引用,不是复制:闭包捕获的是变量本身(或说变量的引用),而不是当时的值。如果在循环中创建闭包,且变量在循环中不断变化,可能出现“所有闭包引用的是同一个变量最后一个值”的问题。
  funcs = []
  for i in range(3):
      def f():
          return i
      funcs.append(f)
  print([f() for f in funcs])   # 输出 [2, 2, 2] 而不是 [0, 1, 2]
  

解决办法是捕获当前值作为默认参数def f(i=i):)或使用工厂函数立即绑定。

  • 不可变类型与赋值:如果闭包内捕获的变量是不可变类型(如数字、字符串),想要“修改”它实际上是在进行重新赋值,这时必须在内部函数里用 nonlocal 声明,否则只会创建一个新的局部变量。

闭包让函数有了“记忆”,在工厂函数、装饰器、回调注册等场景中非常实用。理解了作用域与闭包,你对 Python 的变量访问规则就有了一张完整的“查表地图”。