python中的闭包函数
前言上一类把「闭包是什么」讲清楚的问题落到代码里往往会卡在一个具体写法上内层函数里想改外层的变量为什么一赋值就报UnboundLocalError两个闭包为什么互相串了状态什么时候该写闭包、什么时候该写类这些是「写闭包函数」时真正会遇到的障碍。一个常见的误解是只要内层函数用到了外层的变量就必须写nonlocal。事实不是——只读不写不需要任何声明nonlocal只在你要给这个名字重新绑定赋值、、for循环变量、with ... as等时才需要。另一个误解是闭包必须依赖某种「黑魔法」保持状态实际上它靠的就是函数对象持有的 cell 引用是语言规范里写明的一等机制。本文按「怎么写」来组织先给出闭包函数的语法骨架再讲nonlocal的判定规则然后用几个可以跳出去直接用的例子计数器、累加器、带缓存的函数、配置工厂把用法串起来最后说明闭包与类各自的适用边界。所有示例以 Python 3 为基准nonlocal自 Python 3.0 起可用无需额外版本说明。Python 2.7 已于 2020 年 1 月 1 日停止维护本文不使用任何 Python 2 专有写法。一、闭包函数的语法骨架闭包函数的标准形态是三层外层函数接收参数并定义局部状态内层函数引用这些状态外层函数返回内层函数不调用它。# 适用于 Python 3.0def outer(参数):局部状态 初始值def inner(自己的参数):return 用到局部状态的表达式return inner # 注意返回函数对象不是 inner()最容易写错的一行是最后那句。写return inner()是把内层函数调用结果返回出去闭包就没了。# 适用于 Python 3.0def make_multiplier(n):def multiply(x):return x * nreturn multiply # 返回函数本身double make_multiplier(2)triple make_multiplier(3)print(double(10), triple(10)) # 20 30double和triple各自持有一份独立的n互不影响。每调用一次make_multiplier就产生一个新的 cell 绑定。二、nonlocal的判定规则Python 判断一个名字是局部还是自由靠的是编译期扫描到的赋值动作与运行时顺序无关。# 适用于 Python 3.0def f():x 1def g():print(x) # 只读合法x 是自由变量g()def h():x 1def g():x 2 # 这里有赋值 → x 变成 g 自己的局部变量print(x) # 打印 2g()print(x) # 打印 1外层的 x 根本没被改def k():x 1def g():x 1 # 既是读又是写 → 判定为局部但读的时候还没绑定print(x) # UnboundLocalErrorg()第三种情况才是报错的那种x 1里x先被读而它已被判定为g的局部变量且尚未赋值于是UnboundLocalError。加nonlocal就解决了# 适用于 Python 3.0def make_counter(start0):count startdef counter(step1):nonlocal countcount stepreturn countreturn counterc make_counter(10)print(c()) # 11print(c(5)) # 16nonlocal的一个硬性规则必须能在某个外层函数作用域里找到同名绑定否则编译期就抛SyntaxError不会拖到运行时。所以下面这段连文件都跑不起来# 适用于 Python 3.0def f():def g():nonlocal missing # SyntaxError: no binding for nonlocal missing foundmissing 1return g另外nonlocal只往上找函数作用域不会找到模块级变量要改模块级变量得用global。两者不能同时声明同一个名字。三、闭包与类怎么选闭包和类都能「保存状态 提供操作」区别在复杂度维度闭包类状态数量一两个变量多个字段方法数量通常一个多个需要继承 / 多态不合适合适需要序列化pickle不可直接序列化通常可以需要被反射、调试时看名字只有外层函数名有类名和方法名代码量少多判断标准可以简化成一句话只有一个行为、状态不超过两三个变量用闭包否则用类。闭包还有一个明确的短板——函数对象默认无法被pickle序列化而模块级具名函数和普通类实例可以。实战一累加器与只能读一次的资源# 适用于 Python 3.0def make_accumulator():total 0def add(x):nonlocal totaltotal xreturn totalreturn addacc make_accumulator()print(acc(1), acc(2), acc(3)) # 1 3 6「只加载一次」是闭包很自然的用法用None哨兵表示尚未初始化。# 适用于 Python 3.0def lazy_reader(path):cache Nonedef read():nonlocal cacheif cache is None:with open(path, encodingutf-8) as fp:cache fp.read()return cachereturn readget_text lazy_reader(notes.txt)# 第一次调用才真正读盘之后命中内存print(len(get_text()))print(len(get_text()))注意nonlocal cache不能省cache fp.read()是赋值动作。另外这里没有处理并发多线程同时首次调用会各读一次要严格只读一次用functools.lru_cache3.2或functools.cache3.9更稳。实战二手写一个缓存装饰器装饰器本质上就是「接收函数、返回闭包」的函数这是闭包最主流的落地场景。# 适用于 Python 3.0from functools import wrapsdef memoize(func):cache {}wraps(func) # 保留原函数的 __name__、__doc__ 等元信息def wrapper(*args, **kwargs):key (args, tuple(sorted(kwargs.items())))if key not in cache:cache[key] func(*args, **kwargs)return cache[key]return wrappermemoizedef fib(n):斐波那契数列朴素递归。return n if n 2 else fib(n - 1) fib(n - 2)print(fib(30)) # 832040print(fib.__name__) # fib没有 wraps 会是 wrapperprint(fib.__doc__) # 斐波那契数列朴素递归。cache是wrapper的自由变量所以它会一直活到fib这个函数对象被回收为止。这也是闭包的一个副作用缓存没有上限长期运行会持续占内存。生产代码里应优先用functools.lru_cache(maxsize128)它自带淘汰策略。常见坑点1. 返回时多写了一对括号❌ 错误写法def make_multiplier(n):def multiply(x):return x * nreturn multiply() # TypeError: multiply() missing 1 required argument: x✅ 正确写法def make_multiplier(n):def multiply(x):return x * nreturn multiply # 返回函数对象2. 循环里创建闭包全都读到最后一个值❌ 错误写法handlers []for i in range(3):handlers.append(lambda: i)print([h() for h in handlers]) # [2, 2, 2]✅ 正确写法默认参数在定义时求值或用工厂函数再包一层handlers []for i in range(3):handlers.append(lambda ii: i)print([h() for h in handlers]) # [0, 1, 2]3. 该写nonlocal却只写了global❌ 错误写法def make_counter():count 0def counter():global count # 指向模块级名字外层那个 count 依然没被改count 1return count # NameError: name count is not definedreturn counter✅ 正确写法def make_counter():count 0def counter():nonlocal countcount 1return countreturn counter4. 以为nonlocal能找到模块级变量❌ 错误写法total 0def f():def g():nonlocal total # SyntaxError外层不是函数作用域total 1return g✅ 正确写法模块级变量用globaltotal 0def f():def g():global totaltotal 1return g5. 闭包持有大对象不释放❌ 错误写法def load():big list(range(10_000_000))return lambda: big[0] # big 被 cell 引用无法回收✅ 正确写法只把需要的那点数据捕获进来def load():big list(range(10_000_000))first big[0]return lambda: first # 只捕获 firstbig 可以被回收6. 用闭包做状态还指望能 pickle❌ 错误写法把make_counter()的返回值直接pickle.dumps会抛AttributeError: Cant pickle local object。✅ 正确写法需要序列化就用类把状态放到实例属性里或者只序列化数据、运行时再重建闭包。7. 装饰器里忘了functools.wraps❌ 错误写法def deco(func):def wrapper(*a, **kw):return func(*a, **kw)return wrapper # 被装饰后 __name__ 变成 wrapper__doc__ 丢失✅ 正确写法from functools import wrapsdef deco(func):wraps(func)def wrapper(*a, **kw):return func(*a, **kw)return wrapper8. 闭包与默认参数混用把可变对象当状态❌ 错误写法def make_adder(state[]): # 默认值在函数定义时创建只此一份def add(x):state.append(x)return statereturn add✅ 正确写法状态在外层函数体内创建每次调用外层都拿到新的一份def make_adder():state []def add(x):state.append(x)return statereturn add总结写法结果内层函数只读外层变量合法变量自动成为自由变量内层函数给外层变量赋值但不声明UnboundLocalError先读后写时加nonlocal绑定到最近的外层函数作用域外层不存在同名绑定编译期SyntaxError用于模块级变量用global不能用nonlocalreturn inner()返回调用结果闭包丢失写闭包函数记住三句话返回值不要带括号只读不用声明、要写必须nonlocal循环里要拿当前值就用默认参数或工厂函数。状态超过两三个变量、或者需要多个方法时直接换成类可读性和可调试性都会明显更好。

相关新闻

Python中的面向接口编程示例详解

Python中的面向接口编程示例详解

前言 "面向接口编程"(programming to an interface)的核心主张是:调用方应该依赖"能做什么",而不是依赖"是谁"。这样换实现时不必改调用方,测试时也容易塞进一个假的实现。 在 Java 里&…

2026/10/9 13:20:05 阅读更多 →
GEV-26B-Decide 部署指南:3步为 Gemma-4 的 tied lm_head 打 LoRA 补丁,快速起决策服务

GEV-26B-Decide 部署指南:3步为 Gemma-4 的 tied lm_head 打 LoRA 补丁,快速起决策服务

GEV-26B-Decide 部署指南:3步为 Gemma-4 的 tied lm_head 打 LoRA 补丁,快速起决策服务 【免费下载链接】GEV-26B-Decide 项目地址: https://ai.gitcode.com/hf_mirrors/autotrust/GEV-26B-Decide GEV-26B-Decide 是基于 google/gemma-4-26B-A4B…

2026/10/9 13:20:05 阅读更多 →
微信小程序物业管理系统毕业设计:技术选型、数据库设计与论文写作全指南

微信小程序物业管理系统毕业设计:技术选型、数据库设计与论文写作全指南

简介:这份资源是面向高校计算机相关专业学生的微信小程序物业管理系统毕业设计完整项目,适合作为课程设计、毕业论文或小程序开发练手参考。项目围绕真实小区场景展开,涵盖用户认证与登录、二维码模拟开门、车牌预约审核、物业服务提交、物业…

2026/10/9 13:19:03 阅读更多 →

最新新闻

Manus联合创始人拆解:Claude与阿里千问双模型驱动下的TaoToken统一API接入实践

Manus联合创始人拆解:Claude与阿里千问双模型驱动下的TaoToken统一API接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 13:55:48 阅读更多 →
AI实战:省token妙招之【context-mode】——用SQLite给MCP上下文做减法

AI实战:省token妙招之【context-mode】——用SQLite给MCP上下文做减法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 13:55:48 阅读更多 →
用WPF+OpenCvSharp构建仿VisionMaster拖拉拽视觉框架:交互内核与流程调度拆解

用WPF+OpenCvSharp构建仿VisionMaster拖拉拽视觉框架:交互内核与流程调度拆解

简介:面向工业视觉与上位机开发者的仿VisionMaster通用视觉框架源码包,基于WPF、OpenCvSharp与C#实现流程图式拖拉拽流程编排,开箱即用且便于二次开发。压缩包共2000个文件,约335.39MB,以C#源码(.cs)为核心&#xff0c…

2026/10/9 13:55:48 阅读更多 →
面试中常问的 List 去重问题,你都答对了吗?

面试中常问的 List 去重问题,你都答对了吗?

一道被问烂的面试题:如果你去面试 Java 开发岗位,尤其是初级到中级岗位,十有八九会被问到:「如何对一个 List 进行去重?」很多候选人会脱口而出:「用 HashSet 啊,把 List 丢进去再拿出来不就好了…

2026/10/9 13:55:48 阅读更多 →
Seay源码审计工具实战:从环境搭建到漏洞确认与避坑指南

Seay源码审计工具实战:从环境搭建到漏洞确认与避坑指南

简介:Seay源代码审计系统是一款面向 Web 应用开发者与安全工程师的自动化代码安全审查工具,专注于发现高危函数、注入漏洞和编程错误,并提供一键审计、函数查询、自定义规则、代码高亮与调试、报告生成等核心能力。资源包为Windows环境下常用…

2026/10/9 13:55:48 阅读更多 →
人大金仓KCA/KCP认证备考:从零散题库到可复现的实操路径

人大金仓KCA/KCP认证备考:从零散题库到可复现的实操路径

简介:这份人大金仓KCA、KCP题库整理面向备考金仓数据库认证的考生与数据库运维初学者,聚焦KingbaseES v8核心考点,帮助读者在刷题中快速定位知识盲区、巩固原理细节。内容覆盖系统表存储位置、ORDER BY排序限制、后台进程、模板数据库、索引最…

2026/10/9 13:54:47 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →