← 返回题目列表

Python 闭包是什么?为什么循环里的闭包会出现延迟绑定问题?

高频 中等 第 3 / 21 题 更新于 2026/07/27
闭包延迟绑定作用域工厂函数

简化版

闭包是内部函数引用了外部函数的变量,并且在外部函数执行结束后仍然能访问这些变量。Python 闭包捕获的是变量名绑定关系,不是当时的值,所以循环里创建函数时容易出现延迟绑定问题。

详细版

闭包示例:

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

double = make_multiplier(2)
print(double(10))  # 20

multiply 引用了外层变量 n,即使 make_multiplier 已经执行结束,n 仍然被保留。

高频坑:

funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2]

原因是 lambda 里引用的是变量 i,而不是每次循环时的值。循环结束后 i 的值是 2,所以所有函数调用时都读到 2

常见修复方式:

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)

print([f() for f in funcs])  # [0, 1, 2]

也可以用工厂函数:

def make_func(i):
    def f():
        return i
    return f

完整版教学

一、闭包解决什么问题

闭包可以让函数携带一部分上下文,形成一个“带状态的函数”。

例如生成不同倍数的乘法器:

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

调用:

triple = make_multiplier(3)
print(triple(10))  # 30

triple 本质上是 multiply 函数,但它记住了外层的 n=3

闭包常用于:

  • 装饰器;
  • 回调函数;
  • 函数工厂;
  • 延迟执行;
  • 保存轻量状态。

二、闭包成立的三个条件

一个典型闭包通常满足三点:

  1. 存在嵌套函数。
  2. 内部函数引用外部函数的变量。
  3. 外部函数返回内部函数,或内部函数在外部作用域之外继续被使用。

示例:

def outer():
    msg = "hello"

    def inner():
        return msg

    return inner

inner 引用了 msg,并被返回到外部,所以 msg 不会随着 outer 结束立刻消失。

三、闭包变量存在哪里

可以通过 __closure__ 观察闭包保存的变量:

def outer():
    x = 10

    def inner():
        return x

    return inner

f = outer()
print(f.__closure__[0].cell_contents)  # 10

这说明闭包变量被放在 cell 对象中,内部函数通过这个 cell 找到外层变量。

这也是为什么闭包不是简单复制值。它保留的是对变量所在 cell 的引用。

四、延迟绑定为什么出现

看这个例子:

funcs = []
for i in range(3):
    funcs.append(lambda: i)

循环创建了三个函数,但这些函数体没有立刻执行。等你之后调用:

[f() for f in funcs]

函数才去查找 i。此时循环已经结束,i 最终绑定的是 2,所以三个函数都返回 2

这叫延迟绑定:函数使用变量时才解析变量值,而不是定义函数那一刻就把值固定下来。

五、如何修复延迟绑定

方法一:使用默认参数固定当前值。

funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)

默认参数在函数定义时求值,所以每个 lambda 都有自己的默认值。

方法二:使用工厂函数。

def make_func(i):
    def f():
        return i
    return f

funcs = [make_func(i) for i in range(3)]

每次调用 make_func(i) 都会创建新的局部作用域,每个 f 绑定到不同作用域里的 i

方法三:用 functools.partial 固定参数。

from functools import partial

def identity(i):
    return i

funcs = [partial(identity, i) for i in range(3)]

六、闭包和对象的选择

闭包适合保存少量状态:

def make_counter():
    count = 0

    def inc():
        nonlocal count
        count += 1
        return count

    return inc

如果状态复杂、方法很多、需要继承或清晰的数据结构,就更适合用类:

class Counter:
    def __init__(self):
        self.count = 0

    def inc(self):
        self.count += 1
        return self.count

闭包不是类的替代品,它更像轻量函数状态封装。

七、常见误区与追问

记忆钩子:闭包不是“把值拍照存档”,而是“保留能找到变量的环境”。循环延迟绑定的坑,就来自多个函数最后看向同一个变量 cell。

  • 误区:闭包一定会复制外层变量的值。 Python 闭包保存的是对外层变量 cell 的引用,调用函数时再通过 cell 取值;这就是循环里多个 lambda 最后都读到 2 的根本原因。
  • 追问:为什么默认参数能修复循环闭包? 默认参数在函数定义时求值,lambda i=i: i 会把当次循环的 i 值放进该函数自己的默认参数元组里,不再依赖循环变量后续变化。
  • 误区:只有 lambda 才有延迟绑定问题。def 在循环里定义内部函数同样会遇到这个问题;问题来自闭包变量解析时机,不来自 lambda 语法。
  • 追问:闭包变量能不能修改? 能,但要在内部函数里用 nonlocal 声明,否则 count += 1 会被解释成局部赋值并触发 UnboundLocalError
  • 误区:闭包比类更高级,所以应该优先闭包。 闭包适合少量状态和单一行为;当状态多、行为多、需要继承或调试可见性时,类通常更清晰。
  • 追问:怎么观察一个函数是不是闭包? 可以看 func.__closure__ 是否为 None,以及其中 cell 的 cell_contents;但工程代码不应依赖这个内部细节做业务逻辑。

八、加强记忆

闭包记住的是外层变量所在的环境,不是简单把值复制一份。循环闭包的坑来自延迟绑定:函数调用时才读取变量,所以循环结束后大家读到同一个最终值。要固定当次循环值,就用默认参数、工厂函数或 partial