# 什么是闭包?闭包中的变量为什么会出现延迟绑定?
面试官问:"什么是闭包?下面这段代码为什么输出都是9?"
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2]
2
3
4
5
很多人知道闭包可以"记住外层变量",但说不清楚延迟绑定的原因、闭包如何存储自由变量,以及如何避免循环中的闭包陷阱。
# 简要回答
闭包是一个函数对象,它可以访问其定义时所在作用域的变量,即使外层函数已经返回。这些被闭包引用的外层变量叫自由变量。
闭包中的变量是延迟绑定的。内层函数引用外层变量时,保存的是变量名,不是变量的当前值。调用闭包时,才会查找变量的当前值。
循环中创建闭包时,如果闭包引用循环变量,所有闭包共享同一个循环变量。 循环结束后,循环变量停留在最后一个值,所以所有闭包调用时看到的都是这个最后值。
解决方法:用默认参数立即捕获值,或用functools.partial。
# 详细回答
# 什么是闭包
闭包是一个函数对象,加上它引用的外层作用域变量:
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_three = make_multiplier(3)
print(times_three(10)) # 30
2
3
4
5
6
7
multiplier引用了外层函数make_multiplier的局部变量n。make_multiplier返回后,times_three仍然可以访问n的值3。
# 自由变量与__closure__
被闭包引用的外层变量叫自由变量。Python把自由变量存储在函数对象的__closure__属性中:
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_three = make_multiplier(3)
print(times_three.__closure__) # (<cell at 0x...: int object at 0x...>,)
print(times_three.__closure__[0].cell_contents) # 3
2
3
4
5
6
7
8
__closure__是一个元组,包含多个cell对象,每个cell保存一个自由变量的引用。
# 闭包中的变量是延迟绑定的
闭包保存的是变量名,不是变量的值:
def make_functions():
funcs = []
for i in range(3):
funcs.append(lambda: i)
return funcs
funcs = make_functions()
print([f() for f in funcs]) # [2, 2, 2]
2
3
4
5
6
7
8
为什么都是2?因为:
- 循环创建了3个
lambda函数,每个都引用自由变量i。 - 3个函数共享同一个
i,保存的是对i的引用,不是i的当前值。 - 循环结束后,
i的值是2。 - 调用闭包时,查找
i的当前值,都是2。

# 解决方法1:默认参数立即捕获
默认参数在函数定义时求值,可以立即捕获当前值:
def make_functions():
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # 默认参数立即捕获i的值
return funcs
funcs = make_functions()
print([f() for f in funcs]) # [0, 1, 2]
2
3
4
5
6
7
8
lambda i=i中,右侧的i是循环变量,左侧的i是函数参数。每次循环时,默认参数i=i求值为当前循环变量的值。
# 解决方法2:functools.partial
from functools import partial
def make_functions():
def printer(i):
return i
funcs = []
for i in range(3):
funcs.append(partial(printer, i))
return funcs
funcs = make_functions()
print([f() for f in funcs]) # [0, 1, 2]
2
3
4
5
6
7
8
9
10
11
12
13
partial创建一个新函数,预填充参数。
# 解决方法3:用函数工厂
def make_functions():
def make_lambda(i):
return lambda: i
funcs = []
for i in range(3):
funcs.append(make_lambda(i))
return funcs
funcs = make_functions()
print([f() for f in funcs]) # [0, 1, 2]
2
3
4
5
6
7
8
9
10
11
每次调用make_lambda(i)创建新的局部作用域,lambda引用的是make_lambda的局部变量i,每个闭包有独立的i。
# 闭包修改外层变量
需要nonlocal声明:
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
2
3
4
5
6
7
8
9
10
11
12
13
闭包保存count的引用,nonlocal让increment可以修改这个外层变量。
# 多个闭包共享外层变量
def make_account():
balance = 0
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
balance -= amount
return balance
return deposit, withdraw
deposit, withdraw = make_account()
print(deposit(100)) # 100
print(withdraw(30)) # 70
print(deposit(50)) # 120
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
deposit和withdraw共享同一个balance,通过闭包实现了状态封装。
# 知识拓展
Q1:闭包和嵌套函数是一回事吗?
不是。嵌套函数是定义在函数内部的函数。闭包是引用了外层作用域变量的嵌套函数。没有引用外层变量的嵌套函数不是闭包。
Q2:闭包会导致内存泄漏吗?
闭包保存外层变量的引用,只要闭包存在,外层变量就不会被垃圾回收。如果闭包生命周期很长,且引用了大量数据,可能导致内存占用增加。但这不是泄漏,只是合理的引用保持。
Q3:可以在闭包外查看自由变量的值吗?
可以通过__closure__:
def make_multiplier(n):
def multiplier(x):
return x * n
return multiplier
times_three = make_multiplier(3)
print(times_three.__closure__[0].cell_contents) # 3
2
3
4
5
6
7
但这只用于调试,不应该在业务代码中依赖。
Q4:Python的装饰器是闭包吗?
通常是。装饰器返回的包装函数需要引用原函数,这个包装函数就是闭包:
def my_decorator(func):
def wrapper(*args, **kwargs):
print("before")
result = func(*args, **kwargs) # 引用外层的func
print("after")
return result
return wrapper
2
3
4
5
6
7
wrapper引用外层的func,是闭包。
Q5:JavaScript和Python的闭包行为一样吗?
基本一样,都存在延迟绑定陷阱。JavaScript中循环用var声明变量时,所有闭包共享同一个变量;用let或const时,每次循环创建新的块作用域。Python中推荐用默认参数或函数工厂解决。
评论
验证登录状态...