尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
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循环里要拿当前值就用默认参数或工厂函数。状态超过两三个变量、或者需要多个方法时直接换成类可读性和可调试性都会明显更好。
RELATED

相关推荐

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

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

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

📅 2026/10/9 13:20:11
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:11
MAT内存分析工具Windows版:从原理到实战排查OOM

MAT内存分析工具Windows版:从原理到实战排查OOM

简介:MemoryAnalyzer(MAT)是Eclipse基金会推出的开源Java堆内存分析工具,这款1.6.1版本压缩包构建于2016年11月25日,面向Windows 32/64位环境,主要帮助Java开发者、运维人员与性能调优工程师排查内存泄漏、…

📅 2026/10/9 13:15:09
MORE NEWS

更多资讯

📰

CNN+Transformer混合模型:运动想象脑电分类实战与避坑指南

简介:这份资源是面向计算机、通信、人工智能、自动化等专业学生与从业者的运动想象脑电信号分类Python源码,采用CNN与Transformer结合的框架,通过卷积网络提取局部时间空间特征,再借助Transformer建模长程依赖,可用于毕…

📰

基于OpenCV手势识别的打地鼠游戏:从肤色分割到交互实现

简介:一套完整的人机交互实验项目,面向学习OpenCV、Mediapipe手势识别及交互方式对比的开发者。项目以打地鼠游戏为载体,通过识别食指与中指骨节点位置判定手势,实现光标操作与打击动画地鼠,代码含详细注释。压缩包内共…

📰

Win8/Win10免安装GSQL绿色简版:解压即用与避坑指南

简介:GSQL是一款面向Windows 8与Windows 10的轻量级数据库管理系统,以免安装绿色简版形式提供,解压即可启动服务,适合开发调试、教学演示及临时测试等不希望改动系统配置的场景。压缩包共234个文件,约16.43MB&#xff…

📰

Java多前端心理健康评估系统:量表计分引擎与多端适配实战

简介:这是一套面向高校计算机专业学生与Java Web开发学习者的大学心理健康评估系统完整源码,适合作为课程设计、毕业设计或实战练手项目。系统以Java后端为核心,融合JavaScript、HTML、CSS与PHP等前端技术,构建了涵盖用户登录、身…

📰

卡内基梅隆大学研究者用TaoToken统一Key通道复现“以小博大”智能体路由实验

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

📰

MongoDB聚合管道实战:从$match到$group的常用阶段精讲

做过后端开发的,迟早要和MongoDB的聚合操作打交道。我第一次接触聚合框架时,面对一堆 $match、$group、$sort、$project 完全不知道从哪下手,直到后来接手一个订单统计需求,把常用阶段挨个用了一遍,才算真正开窍。这篇…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬