返回函数
高阶函数的“反向输出”能力
之前我们聊过高阶函数可以把函数当成参数传进去,用来抽象通用逻辑。但高阶函数还有一个更有趣的方向:把函数当成结果返回。
这种“先封装逻辑,后传入数据,延迟触发”的设计,能帮我们实现延迟计算、动态生成相似函数,甚至封装状态变量——这就引出了 Python 里经典又实用的闭包概念。
从一个求和场景看“延迟触发”
先看最直观的延迟计算例子:我们需要一次性收集一堆数字,但并不想立刻算出总和,而是等真正要用的时候再执行计算。
普通版:立即计算
这种方式简单直接,但每次调用都会立刻算出结果,缺少“等到合适时机再算”的弹性。
高阶版:返回函数再调用
我们可以把“累加逻辑”和“触发时机”拆开:
调用方式变成了两步:
- 传入参数,得到一个“专属计算函数”
- 在需要的时候显式调用它,才真正执行计算
这种模式就像“把菜谱写在纸条上,收好食材清单,等到饭点再按方子做菜”——逻辑和参数都被封存起来了。
闭包(Closure)到底是什么?
上面 lazy_sum 里的 sum_logic,就是一个典型的闭包:
闭包是指一个内部函数,引用了外部函数的变量或参数,即使外部函数已经执行完毕,这些被引用的变量仍然会“存活”在内部函数身上,不会被回收。
换句话说,闭包能“记住”自己出生时的外部环境。
两个重要特性
1. 每次调用外部函数,都会创建全新的闭包实例
虽然用相同的参数创建,但每次返回的函数对象都是独立的。
2. 闭包记住的是变量的引用,而不是当时的值
这个特性既是灵活性的来源,也是常见陷阱的根源,接下来我们会重点讨论。
闭包的经典陷阱:循环变量引用
如果闭包直接引用一个循环变量,写出来的代码往往和直觉不符。
踩坑示例
假设我们想生成 3 个函数,分别返回 0²、1²、2²:
为什么全是 4?
因为 Python 的变量是延迟绑定的:
- 循环结束后,外部变量
i已经变成了2(最后一次循环的值) - 闭包
f内部保存的是i的引用,而不是那个时刻的副本 - 当我们真正调用
f0()时,才会去查找i的当前值,于是全部拿到2²=4
解决陷阱的两种经典方案
核心思路是:让闭包在创建时就“记住当时的变量值”,而不是一直保存引用。
方案一:再加一层辅助函数
每次循环时,用辅助函数接收当前 i 的值作为参数,闭包引用的是辅助函数的参数拷贝:
方案二:利用默认参数(推荐写法)
Python 函数的默认参数在定义时就已经绑定,利用这个特性可以写得非常简洁:
⚠️ 注意:默认参数建议使用不可变类型(如
int、str、tuple)。如果用可变类型(如list、dict),会产生新的默认参数共享陷阱。
修改外部变量:nonlocal 声明
前面闭包只是读取外部变量,如果我们要修改外部的不可变变量(int、str 等),Python 默认会将赋值视为定义局部变量,从而报错。
错误演示
解决方法:加上 nonlocal
用 nonlocal 明确告诉 Python:“这个变量不是局部的,也不是全局的,是外层函数里的变量”。
📌 补充:如果外部变量是可变类型(如
list、dict),你可以直接调用list.append()、dict['key'] = value来修改内容,无需nonlocal。但如果你想重新赋值整个变量(如list = []),则依然需要nonlocal。
现代 Python 的简化写法:海象运算符 :=(Python 3.8+)
对于这种简单的计数器场景,Python 3.8 引入的海象运算符可以让代码更紧凑:
:= 可以在表达式中完成赋值并返回新值,非常适合编写“单行闭包”。
闭包的最佳实践
-
循环变量慎用直接引用
遇到循环里的闭包,优先使用默认参数绑定当前值,或加一层辅助函数。 -
修改外部不可变变量必须声明
nonlocal
可变类型可以修改内部元素,但重新赋值整个变量也需要nonlocal。 -
避免在闭包中长期持有大对象引用
闭包会让引用的对象一直存在于内存中,可能导致内存泄漏,尤其是在长期运行的场景里。 -
优先用闭包实现轻量的“函数工厂”
比如生成不同税率的计算函数、不同阈值的过滤函数,比定义类更轻便。
总结
返回函数是高阶函数的重要应用之一,而闭包是这种用法的灵魂:
- 它能记住创建时的外部环境,实现灵活的延迟计算
- 它能封装独立的状态变量,做出轻量的“带状态”工具
- 它还是 Python 装饰器的底层实现基础(我们下次再聊装饰器!)
掌握好闭包,你就能写出更优雅、更灵活的 Python 函数式代码。

