news 2026/10/17 2:44:28

Python 闭包:为什么函数能记住早就销毁的变量

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python 闭包:为什么函数能记住早就销毁的变量

我感觉的闭包

刚学 Python 的时候,跟着课程走,感觉 Python 就那样也不难啊,直到我碰到了闭包。闭包是我感觉从学 Python 开始,用的时间最长的一小节课程 ,44分钟的课程反反复复看了好几遍。

后来真的弄明白了,才发现它朴素得有点无聊——无非是"函数记住了自己出生时的环境"。顿时感觉OMG,自己在干嘛,废了废了,浪费了大把时间在纠结一个非常简单的东西,这篇就按我自己的理解顺序捋一遍。


一、先看一段经典的代码示例

defouter():msg="hello"definner():print(msg)returninner f=outer()f()# hello

如果你已经习惯 Python,可能一扫就过去了。但稍微停一下想想:

outer()已经执行完了,msg是它的局部变量。按正常理解,函数返回时msg这个局部变量应该跟着栈帧一起被回收。那f()打印出来的"hello"是哪来的?

答案就是:inner把这个变量"拎"出来了,带在自己身上。

这是闭包的起点。一句话概况:

闭包 = 内层函数 + 它记住的那部分外层环境。


二、它是怎么记住的

Python 里函数是一等公民,它可以是对象,这一点很多人知道。但其实这个对象上还挂着一个属性,专门放"外面的变量":

defouter():msg="hello"definner():print(msg)returninner f=outer()print(f.__closure__)# (<cell at 0x000001F2E4C1B180: str object at 0x000001F2E4C0A8B0>,)print(f.__closure__[0].cell_contents)# hello

那个cell你可以理解成一个小盒子。变量msg没被销毁,而是被装进盒子,跟着inner函数对象一起走了。

我第一次看到这个的时候还挺意外:原来变量不是消失了,是搬家了。

所以f()每次执行,去自己的盒子里取msg,自然拿得到。

顺带一提,不是外层所有变量都会被装盒,只有真正被内层用到的才会。这个下面还会提。

换句话说: 就可以想象一个背包客带着有用的家当离家旅游,回来发现家没了,但是,不是有句话叫做:“人在家就在”,背包客(内层函数)和 家当(变量)在一起又是一个新家。


三、构成闭包的条件

按教科书说法是三条,我按自己好记的顺序排了:

  1. 函数里面定义了函数(嵌套)
  2. 内层函数用到了外层的变量
  3. 外层函数返回的是内层函数

第 2 条是关键。如果inner里没提msg,那就没有闭包,__closure__会是None。

第 3 条其实是说:内层函数得"活下来",跑到外面去用。如果inner()在外层函数里就调用完了,那也没什么好记的。


四、为什么要有这东西

因为有些状态,你不想为它专门写个类。

比如我有个需求:做个计数器,每次调用加一,而且不能让别人从外面改这个数。

用函数写是长这样:

defcounter():n=0defstep():n+=1returnnreturnstep c=counter()print(c())# ?print(c())# ?

先别急着往下看,猜猜输出是什么。

实际上它会直接报错:

UnboundLocalError: cannot access local variable 'n' where it is not associated with a value

原因是n += 1等价于n = n + 1。只要函数体里出现了对n的赋值,Python 在编译期就把n认定为step的局部变量,而不是外层那个。于是第一件事就是读一个还没被赋值的局部变量,报错。

怎么修?告诉 Python:“我要改的是外层的那个 n。”

这个时候就要用到nonlocal这个关键字了

defcounter():n=0defstep():nonlocaln n+=1returnnreturnstep c=counter()print(c())# 1print(c())# 2print(c())# 3

nonlocal就是干这个的。
有了nonlocal修饰就可以放心的更改外层函数的变量了,提到nonlocal就很容易让人想起另外一个关键字global,而且global跟nonlocal有着相同的作用,但是作用范围不一样:

  • nonlocal:找外层函数里的变量,逐层向外,不涉及全局
  • global:直接指向(全局)变量

一个常见错误是在模块级函数里用nonlocal,会直接报SyntaxError,因为没有外层函数可找。


五、经典陷阱:循环里的闭包

funcs=[]foriinrange(3):funcs.append(lambda:i)forfinfuncs:print(f())

这真是一道很经典的示例,不妨猜猜看它的输出是什么?

如果你觉得是0 1 2,那你跟我第一次见的时候一样天真。那是一道选择题,看一眼我直接选择了0 1 2这个答案。实际是:

2 2 2

为什么?

因为闭包捕获的是变量本身,不是它当时的值。三个 lambda 用的是同一个i(也就是同一个 cell),循环结束时i停在 2,所以三个函数一起打印 2。

想固化当时的值,就得在创建函数的时候把这个值"复制"一份进去。最常见的是用默认参数:

funcs=[]foriinrange(3):funcs.append(lambdai=i:i)# 注意这里的 i=iforfinfuncs:print(f())# 0 1 2

默认参数在函数定义时求值,所以每次循环都存了一份当下的i。


六、闭包和类,该用哪个

这是绕不过去的问题。实话说,绝大多数情况下,闭包能做的类也能做,反过来也成立。

# 闭包版defcounter():n=0defstep():nonlocaln n+=1returnnreturnstep# 类版classCounter:def__init__(self):self.n=0defstep(self):self.n+=1returnself.n

我的实际取舍是看状态量和调试需求:

  • 状态就一两个、逻辑也就一小段,用闭包,短
  • 状态多了、方法多了、需要print出来看内部、需要序列化、需要被别人继承或者扩展——老老实实写类
  • 闭包的状态藏在__closure__里,调试器默认不给你展开,出问题时你会在print里来回猜。类就不一样,obj.n一眼就看到了

所以网上那些"闭包性能更好、应该替代类"的说法,我听一半,有的时候一味的节省时间未见得是什么好事


七、几个零碎的注意点

闭包捕获的是变量,不是值。前面循环那个坑就是这条的直接后果。

闭包会延长变量的生命周期。被装进 cell 的东西,只要函数对象还在,就不会被回收。如果外层闭的是个大列表、大图片、数据库连接,那它就跟着一直活着。

只有被用到的变量才进闭包。想看具体哪些,可以:

defouter():a=1b=2definner():returna# 只用了 areturninner f=outer()print(f.__code__.co_freevars)# ('a',)print(f.__closure__)# 只有一个 cell

b没被引用,就不会被记住。

查看是否真的形成了闭包,最直接的就是f.__closure__,None说明没有。


八、收个尾

闭包这东西,说白了就一句:函数带着它记得的环境到处跑。

真正需要记住的坑也就两个:

  1. 想改外层变量,得写nonlocal
  2. 循环里创建函数,小心变量是共享的

剩下的都算自己的手感,多多练习几次自然就熟了。

如果你看到这里还有一点懵,我建议就手敲几遍,让AI生成也罢,自己找题也罢,一定不要看答案或者让AI直接给出代码,自己敲一遍,报错再看正确的写法,这样反复来上一两遍,比看十篇解释都管用。

学习嘛,就是这么个过程。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 2:44:14

page_alloc build_all_zonelists

build_all_zonelists() 是内核中 zonelist 构建的统一入口函数&#xff0c;负责在系统启动和内存热插拔两种场景下&#xff0c;为 NUMA 节点建立内存分配时的 zone 尝试顺序。核心作用与定位zonelist 决定了分配内存时按什么顺序尝试各个 zone 和节点。build_all_zonelists() 根…

作者头像 李华
网站建设 2026/10/17 2:44:08

2026年大厂Agent开发岗真实门槛深度拆解,小白程序员必备!

本文深度剖析了2026年大厂Agent开发岗的真实门槛&#xff0c;指出会LangChain、LangGraph或跑通Agent Demo并不足以胜任工作。文章强调工程能力和业务思维的重要性&#xff0c;分析了架构设计、Tool Calling、线上稳定性、项目深挖等方面的关键要求&#xff0c;并提出了提升竞争…

作者头像 李华
网站建设 2026/10/17 2:42:43

FastReport 2022-3 VCL源码集成与跨Delphi版本定制指南

简介&#xff1a;本资源为FastReport 2022-3 VCL Enterprise完整源码包&#xff0c;专为Delphi中高级开发者设计&#xff0c;适用于D7至D12全版本VCL框架下的报表深度定制与二次开发场景。资源包含2000个文件&#xff0c;主体为559个Pascal源码&#xff08;.pas&#xff09;、7…

作者头像 李华
网站建设 2026/10/17 2:42:41

SolidJS 由 Ryan Carniato 发起,采用 MIT 开源协议,社区维护

2026 年的前端框架格局已经发生了深刻变化。React 用 Server Components 重新定义了组件模型&#xff0c;Vue 3.5 用 Vapor 模式抛弃了虚拟 DOM&#xff0c;Svelte 5 用 Runes 统一了响应式语法。 但在众多框架中&#xff0c;SolidJS 以其独特的"细粒度响应式"理念脱…

作者头像 李华
网站建设 2026/10/17 2:41:47

goDataFake数据使用说明

## DataFake 使用说明DataFake 是一个用于生成模拟测试数据的工具类&#xff0c;主要用于生成试卷&#xff08;Papers&#xff09;、句子&#xff08;Sentences&#xff09;和单词标记&#xff08;Tokens&#xff09;的假数据。### 1. 获取实例通过依赖注入获取单例&#xff1a…

作者头像 李华