很多Python学习者对“闭包”一词既熟悉又陌生:能背出定义,但一写代码就迷糊。今天我们就用最接地气的方式,把它从底层原理到实战用法彻底讲透。读完你会清楚:
1. 什么是闭包
闭包 = 内层函数 + 被内层函数所引用的外层变量
只要满足三个条件,闭包就诞生了:
函数嵌套:一个函数内部定义了另一个函数
内层引用:内层函数使用了外层函数的变量(或参数)
外层返回:外层函数把内层函数作为返回值交出来
只有把内层函数交出去,那些被引用的外层变量才不会随着外层函数的结束而消亡,闭包才真正“活”下来。
2. closure 元组与闭包单元
我们说的“闭包”,在Python内部其实是一个元组。def out(x): y = 10 def inner(): return x + y return innerf = out(5)print(f.__closure__) # 输出类似:(<cell at 0x10...: int object at 0x...>, <cell at 0x...: int object at 0x...>)print(len(f.__closure__)) # 2print(f.__closure__[0].cell_contents) # 5print(f.__closure__[1].cell_contents) # 10
解读:
out函数中,被inner用到的变量 x 和 y,会被封存在闭包单元(cell)里。
这些cell会组成一个__closure__元组,最终挂在内层函数 inner 身上。
每个cell可以通过 .cell_contents 查看当前保存的值。
这就解释了为什么外层函数执行完了,变量还能“活着”——它们其实被搬到了内层函数的__closure__里。
小提示:如果内层函数没有引用任何外层变量,它的 __closure__ 就是 None。
3. 两个重要特性
特性1:调用N次外层函数,得到N个互不影响的闭包
每次调用外层函数,都会创建一套全新的局部变量,所以闭包之间的状态是隔离的。
def counter(start=0): count = start def inc(): nonlocal count count += 1 return count return incc1 = counter(10)c2 = counter(10)print(c1()) # 11print(c1()) # 12print(c2()) # 11 ← c2完全不受c1影响
c1 和 c2 各自拥有自己的 count 变量,就像两个独立的计数器。特性2:外层变量是可变对象,闭包之间依然互不影响
就算捕获的是列表、字典等可变对象,只要它们来自不同的外层函数调用,闭包之间也是隔离的。
def recorder(): log = [] # 可变对象 def add(item): log.append(item) return log return addr1 = recorder()r2 = recorder()print(r1('a')) # ['a']print(r1('b')) # ['a', 'b']print(r2('x')) # ['x'] ← r2有自己的log列表,完全独立
关键点:隔离的本质是因为每次调用 recorder() 都生成了全新的 log 列表。只要不是同一个闭包实例多次修改同一个可变对象,它们就不会打架。
4. 闭包的四大优点
1. 记住状态:不用全局变量,也不用类
做游戏计数器、缓存、累加器时,闭包可以帮你“记住”上次的结果。
def make_accumulator(): total = 0 def add(n): nonlocal total total += n return total return addacc = make_accumulator()print(acc(5)) # 5print(acc(3)) # 8
没有全局变量污染,也没有写class,状态就保留下来了。2. 定制“配置过的函数”:先传参,后使用
有些函数需要一部分参数固定不变,闭包可以帮你制造“高配版”函数。def multiply_by(factor): def multiply(number): return number * factor return multiplydouble = multiply_by(2)triple = multiply_by(3)print(double(5)) # 10print(triple(5)) # 15
double 就是一个被“配置”成永远乘2的函数。这个模式在回调、数据处理流水线中非常实用。3. 实现简单的数据隐藏
外层变量对外界是不可见的,只能通过返回的内层函数间接操作,等于给数据加了一层保护。def bank_account(initial): balance = initial def deposit(amount): nonlocal balance balance += amount return balance def withdraw(amount): nonlocal balance if amount <= balance: balance -= amount return balance else: return "余额不足" return deposit, withdrawdep, wd = bank_account(100)print(dep(50)) # 150# 外部根本无法直接访问 balance 变量
4. 装饰器的基石
Python装饰器本质上就是闭包。理解了闭包,再看 @timer、@login_required 这类语法就会恍然大悟。
五、闭包的三个缺点(清醒使用)
理解成本高对团队里的新手不太友好,过度嵌套会让代码可读性急剧下降。
可能无意中占用大内存如果闭包捕获了一个巨大的对象(如大列表、大DataFrame),而且闭包长期驻留,这块内存就一直释放不了。
类+实例属性有时更清晰闭包能记住状态,一个类的实例也能。如果逻辑开始变复杂(多个方法、状态多),用类往往比多层闭包更容易维护。
# 同样的累加器,用类实现可能更直观class Accumulator: def __init__(self): self.total = 0 def add(self, n): self.total += n return self.total
六、总结
理解闭包,不只是为了应付面试,而是真正掌握Python函数式编程的一把钥匙。