打爆Python 闭包的狗头
很多初学 Python 的朋友都会有一个疑问函数执行完以后里面的局部变量不是应该消失了吗那为什么有些函数还能“记住”之前的数据这个问题背后涉及 Python 里一个非常重要的概念闭包。闭包听起来有点抽象但它其实并不神秘。简单来说闭包就是一个函数记住了它定义时所在环境中的变量。今天我们就用一篇文章把 Python 闭包讲清楚。一、先看一段容易猜错的代码我们先来看一段代码defouter():x10definner():nonlocalx x1returnxreturninnerprint(outer()())print(outer()())你觉得输出结果是什么很多人看到这种“函数里面嵌套函数”的写法会马上想到闭包。于是可能会认为inner()会记住外层函数里的变量x第一次调用时x从 10 变成 11第二次调用时x应该从 11 变成 12。所以很多人的第一反应是输出1112但实际结果是1111这就有点奇怪了。不是说闭包可以记住外层变量吗为什么这里看起来没有记住呢我认为这其实正是理解闭包的关键之处。继续看下去吧不难。二、为什么outer()()两次都是 11关键在于print(outer()())print(outer()())这两行代码中outer()被调用了两次每调用一次outer()都会创建一套全新的局部环境。也就是说每次执行outer()时都会重新创建一个新的xx10同时也会重新创建一个新的inner()函数。所以第一行print(outer()())可以理解成temp_funcouter()print(temp_func())这里发生了什么第一步调用outer()temp_funcouter()这时创建了一个新的x初始值是10然后返回一个inner函数。第二步调用这个返回的函数print(temp_func())于是x 1结果变成11。所以第一行输出11第二行print(outer()())又重新调用了一次outer()。这一次不是继续使用上一次的x而是又创建了一个全新的x 10。所以第二次也是10111因此输出结果是1111可以用一个比喻来理解每次执行outer()()就像重新买了一台新的计数器。每台计数器的初始值都是 10。你每次只按了一下所以每次都是 11。所以这里并不是闭包失效了而是你每次都创建了一个新的闭包。它们之间互不影响。三、正确用法先把返回的函数保存下来如果我们希望闭包真正“记住上一次的值”就不能每次都重新调用outer()。正确的写法是defouter():# 外层函数中的局部变量# 正常情况下outer() 执行结束后x 应该被销毁# 但因为 inner() 使用了它所以它会被闭包保存下来x10definner():# nonlocal 表示# 这里使用的 x 不是 inner() 自己的局部变量# 而是外层函数 outer() 里的 x。## 易错点# 如果要在 inner() 里面修改外层变量 x# 就必须写 nonlocal x。# 否则 Python 会把 x 当成 inner() 内部的新局部变量# 从而导致报错。nonlocalx# 每调用一次 inner()都会让同一个 x 加 1x1# 返回修改后的 xreturnx# 返回 inner 函数本身而不是执行 inner()## 易错点# 这里写的是 return inner不是 return inner()## return inner# 把函数本身返回出去后面可以继续调用。## return inner()# 立刻执行 inner 函数并返回执行结果。returninner# outer() 执行后返回 inner 函数# counter 接住了这个 inner 函数## 关键点# counter 保存的是一个“带着 x 的函数”# 这就是闭包counterouter()# 第一次调用 counter()# 实际上是在调用 inner()# x 从 10 变成 11print(counter())# 第二次调用的还是同一个 inner()# 所以 x 会在上一次的基础上继续加 1# x 从 11 变成 12print(counter())# 第三次调用的仍然是同一个 inner()# x 从 12 变成 13print(counter())输出结果111213这段代码的关键不是inner()里面的逻辑有多复杂而是这一句counterouter()这一句做了两件事第一执行outer()。第二把outer()返回的inner函数保存到变量counter中。从这以后我们每次执行counter()其实都是在调用同一个inner()函数。既然是同一个inner()函数那么它引用的也是同一个x。所以x会持续累加10-11-12-13而不是每次都从 10 重新开始。四、到底什么是闭包闭包其实并不神秘。在 Python 中如果满足下面几个条件就可以形成闭包有一个外层函数外层函数里面定义了一个内层函数内层函数使用了外层函数中的变量外层函数把内层函数返回出去返回出去的内层函数仍然可以访问外层函数的变量。例如defouter():x10definner():returnxreturninner这里的inner就是一个闭包。因为inner定义在outer里面inner使用了外层函数中的变量xouter把inner返回了出去即使outer()执行结束inner仍然能访问x。我们可以把闭包理解成闭包 函数 它记住的外部变量或者更形象一点闭包就像一个随身带着“记事本”的函数。函数负责执行逻辑记事本负责保存数据。在计数器例子中counterouter()得到的counter就不是一个普通函数。它是一个带着变量x的函数。这个x就是它的“记忆”。五、为什么外层函数结束后变量还活着很多人最疑惑的地方是x明明是outer()里的局部变量为什么outer()执行完之后x没有被销毁按照普通理解函数执行结束后里面的局部变量应该被清理。这句话本身没有错。但是闭包是一个特殊情况。原因是外层函数中的变量被内层函数引用了。在普通函数里局部变量只在函数执行期间存在。例如deftest():a100returna当test()执行结束后如果没有其他地方引用a那么这个局部变量对应的对象就可能被回收。但是在闭包中情况不同。例如defouter():x10definner():returnxreturninner funcouter()这里outer()虽然执行完了但是它返回的inner被func保存了下来。而inner又引用了外层变量x。所以 Python 不能直接把x清理掉。因为inner以后可能还要用它。六、闭包背后的 cell 对象Python 为了让内层函数在外层函数结束后仍然能访问外层变量会把这些被引用的变量保存到一个特殊的对象里。这个对象通常叫做cell 对象。可以简单理解成func / counter ↓ inner 函数 ↓ 闭包中的 cell 对象 ↓ 外层变量 x也就是说x并不是简单地作为普通局部变量存在。它被 Python 放进了闭包相关的 cell 对象中。只要内层函数还存在这个 cell 对象就还存在。只要 cell 对象还存在里面保存的x就还存在。所以闭包能记住外层变量并不是魔法。它背后是 Python 帮我们保存了这些变量。七、再从垃圾回收机制理解一下在 Python 中对象什么时候会被回收主要取决于它还有没有被引用。Python 的内存管理中有一个非常重要的机制叫做引用计数。简单来说如果一个对象还被其他变量、函数或对象引用着它就不会被当成垃圾回收。来看这个例子counterouter()执行完之后引用关系大概是这样的counter ↓ inner 函数 ↓ 闭包中的 cell 对象 ↓ x 对应的对象所以只要counter还存在inner函数就还存在只要inner函数还存在它引用的闭包变量就还存在只要闭包变量还存在x对应的对象就不会被回收。因此虽然outer()已经执行结束了但x并不会马上消失。因为在 Python 看来x还“有人用”它不是垃圾。只有当这条引用链断开时相关对象才有机会被回收。例如delcounter当counter被删除后如果没有其他变量再引用这个闭包函数那么inner函数以及它保存的闭包变量x才有机会被 Python 的垃圾回收机制清理掉。所以要注意闭包的本质不是“局部变量永远不会销毁”。而是“局部变量被内层函数引用后会随着闭包一起继续存在”。只要闭包还被外部引用这些变量就不会被回收。八、用__closure__看看闭包里保存了什么我们可以通过函数的__closure__属性查看一个函数是否携带了闭包变量。例如defouter():x10definner():returnxreturninner funcouter()print(func.__closure__)print(func.__closure__[0].cell_contents)输出类似(cell at0x000001F2A3B4C5D0:intobjectat0x00007FF...,)10这里的func.__closure__保存的就是闭包引用的变量信息。而func.__closure__[0].cell_contents可以查看 cell 对象里具体保存的内容。在这个例子中看到的是10这就说明outer()虽然已经执行结束了但变量x仍然被inner函数记住了。如果一个函数没有形成闭包那么它的__closure__通常是None。例如defhello():returnhelloprint(hello.__closure__)输出None说明这个函数没有携带外部变量。九、nonlocal是做什么用的在前面的计数器代码里我们用了nonlocaldefouter():x10definner():nonlocalx x1returnxreturninner为什么要写nonlocalx原因是我们在inner()里面不仅要读取x还要修改x。如果只是读取外层变量不需要写nonlocal。例如defouter():namePythondefinner():returnnamereturninner这里inner()只是读取name没有修改它所以不需要nonlocal。但是如果你要修改外层变量就必须写nonlocal。如果不写会发生什么defouter():x10definner():x1returnxreturninner counterouter()print(counter())运行后会报错UnboundLocalError:local variablexreferenced before assignment为什么因为 Python 在分析函数时只要看到函数内部有赋值操作比如x1它就会认为x是当前函数inner()的局部变量。但问题是x1本质上相当于xx1右边需要先读取x然后再加 1最后赋值给x。可是 Python 已经把x当成inner()的局部变量了而这个局部变量在读取之前还没有被赋值。所以就报错了。加上nonlocalx就是明确告诉 Python这个x不是inner()里的局部变量而是外层函数outer()里的变量。我要修改的是外层那个x。所以总结一下只读取外层变量不需要nonlocal。要修改外层变量必须使用nonlocal。十、return inner和return inner()的区别闭包里还有一个非常容易出错的地方returninner和returninner()这两个写法差别非常大。1.return innerdefouter():x10definner():returnxreturninner这里返回的是函数本身。所以外部可以这样接收funcouter()print(func())也就是说func现在就是inner函数。你可以以后再调用它。这正是闭包常见的写法。2.return inner()defouter():x10definner():returnxreturninner()这里返回的不是函数而是inner()的执行结果。也就是说outer()执行时会马上调用inner()然后把结果返回出去。例如resultouter()print(result)输出10这时result保存的是数字10不是函数。所以你不能再写result()否则会报错TypeError:intobjectisnotcallable所以一定要记住return inner返回函数本身形成闭包。return inner()立即执行函数返回执行结果。十一、闭包有什么用闭包最常见的作用是保存状态。也就是说让一个函数在多次调用之间保留一些数据。1. 用闭包实现计数器defmake_counter():count0defcounter():nonlocalcount count1returncountreturncounter countermake_counter()print(counter())print(counter())print(counter())输出123这里的count就是被闭包保存起来的状态。它不会暴露成全局变量也不会被外部随意修改。如果不用闭包我们可能会写成全局变量count0defcounter():globalcount count1returncount这样虽然也能实现计数但count是全局变量任何地方都可以修改它代码变复杂后就容易出问题。闭包的好处是既能保存状态又能避免使用全局变量。2. 用闭包生成定制函数比如我们想生成一个“乘法器”defmake_multiplier(n):defmultiplier(x):returnx*nreturnmultiplier doublemake_multiplier(2)triplemake_multiplier(3)print(double(10))print(triple(10))输出2030这里doublemake_multiplier(2)得到的是一个记住了n 2的函数。所以double(10)相当于10*2而triplemake_multiplier(3)得到的是一个记住了n 3的函数。所以triple(10)相当于10*3这就是闭包的一个典型应用根据不同参数生成不同功能的函数。3. 用闭包封装私有变量闭包还可以用来隐藏数据避免变量被外部直接修改。例如我们写一个简单的钱包defcreate_wallet():money100defpay(amount):nonlocalmoneyifamountmoney:money-amountreturnf支付成功余额{money}else:return余额不足returnpay walletcreate_wallet()print(wallet(30))print(wallet(50))print(wallet(30))输出支付成功余额70支付成功余额20余额不足在这个例子中money是外层函数里的变量。外部不能直接访问它。我们不能直接写money100000去修改闭包内部的money。外部只能通过wallet(30)间接操作它。这就实现了一种简单的数据封装。当然在 Python 中更常见的封装方式是类。但闭包也可以在一些轻量场景下起到类似“私有变量”的效果。十二、闭包的优点和缺点闭包很好用但也不是没有代价。优点一可以保存状态闭包可以让函数记住上一次执行后的数据。这非常适合计数器缓存配置函数回调函数装饰器。例如计数器中的count就会随着函数多次调用一直保存下来。优点二可以减少全局变量如果不用闭包很多状态可能会被写成全局变量。全局变量一多代码就容易混乱。而闭包可以把变量藏在函数内部只通过返回的内层函数来操作它。这样可以减少变量污染也能降低被误修改的风险。优点三可以生成定制函数通过外层函数传入不同参数可以生成不同功能的函数。比如doublemake_multiplier(2)triplemake_multiplier(3)这两个函数逻辑相似但携带的参数不同所以行为也不同。这在函数式编程中非常常见。缺点一可能增加内存占用闭包会让外层函数中的某些变量继续存在。如果闭包引用了很大的对象并且闭包一直没有被释放那么这些对象也不会被垃圾回收。例如defouter():big_data[0]*10000000definner():returnlen(big_data)returninner funcouter()只要func还存在big_data就不会被回收。所以闭包虽然方便但也要注意不要无意中长期引用大对象。缺点二逻辑可能不够直观闭包中的变量生命周期比普通局部变量更复杂。普通局部变量一般随着函数执行结束就消失。但闭包变量会因为被内层函数引用而继续存在。对于初学者来说如果闭包嵌套得太多代码就会变得不好理解。所以闭包适合用在确实需要“保存状态”或者“生成函数”的场景中。不建议为了炫技而滥用。十三、再回头看最开始的问题现在我们再回头看最开始这段代码defouter():x10definner():nonlocalx x1returnxreturninnerprint(outer()())print(outer()())为什么输出是1111因为每次执行outer()()都会重新调用一次outer()。每次调用outer()都会创建一个新的x 10也会创建一个新的inner函数。所以两次调用之间并没有共享同一个x。而下面这种写法counterouter()print(counter())print(counter())print(counter())输出111213是因为counterouter()只调用了一次outer()。返回的inner函数被counter保存下来。后面每次调用counter()都是在调用同一个inner。所以它操作的是同一个x。这就是闭包能够“记住状态”的真正原因。十四、一句话总结闭包闭包的核心可以用一句话概括闭包就是一个函数记住了它定义时所在环境中的变量。普通函数执行完后局部变量通常就结束了。但闭包不一样。如果外层函数中的变量被内层函数引用并且内层函数被返回到外部继续使用那么这些变量就会随着闭包一起继续存在。它就像给函数装了一个“记忆胶囊”。函数负责执行逻辑。闭包变量负责保存状态。如果你看到这种结构defouter():x10definner():returnxreturninner就可以想到inner不只是一个函数它还带着外层变量x的记忆。理解了闭包后面再学习装饰器、函数式编程、回调函数就会轻松很多。