返回函数
高阶函数的“回退”能力
之前我们聊过高阶函数可以把函数当参数塞进去做逻辑抽象,但高阶函数还有个更有意思的反向特性:把函数当结果吐出来。
这种「先存逻辑、后存数据/参数、按需触发」的设计,能帮我们实现延迟计算、动态生成相似函数、甚至封装状态变量——这就引出了Python里经典的「闭包」概念。
从立即求和到延迟触发的例子
先看最直观的延迟计算场景:假设我们需要先收集一批数,但不想立刻算总和,等真正需要用的时候再跑。
普通版:立即计算
# 把可变长参数直接传,循环累加立刻返回结果
def calc_sum(*args):
total = 0
for n in args:
total += n
return total
# 调用后立刻拿到25
calc_sum(1, 3, 5, 7, 9)
高阶版:返回函数再调用
我们可以把「累加逻辑」和「触发时机」拆分开:
def lazy_sum(*args):
# 内部定义一个真正的累加函数,它会引用外部的args
def sum_logic():
total = 0
for n in args:
total += n
return total
# 直接返回sum_logic的函数对象,不执行!
return sum_logic
调用方式也变了两步:
- 收集参数+生成专属函数对象
- 显式调用函数对象才计算
# 第一步:返回的是<function lazy_sum.<locals>.sum_logic at 0x...>
sum_worker = lazy_sum(1, 3, 5, 7, 9)
# 第二步:真正执行sum_logic,拿到25
sum_worker()
闭包(Closure)到底是什么?
刚才lazy_sum里的sum_logic,就符合闭包的完整定义:
一个内部函数,引用了外部函数的变量/参数,即使外部函数执行完毕已经离开栈,这些被引用的变量仍然会被“绑定”在内部函数上,不会被Python的垃圾回收器清理。
闭包的两个直观特性
我们可以用代码验证:
1. 每次调用外部函数,都会生成全新的闭包实例
# 生成两个用相同参数但不同内存地址的闭包
worker1 = lazy_sum(1, 3, 5)
worker2 = lazy_sum(1, 3, 5)
# 函数对象本身不相等(不同的内存地址)
print(worker1 == worker2) # 输出 False
# 但触发后的计算结果是一样的
print(worker1() == worker2()) # 输出 True
2. 闭包会严格记住创建时的外部环境
这个环境不只是“当时的变量值”吗?先别急,后面会讲它的「陷阱」,其实默认记的是「变量的引用」。
闭包的经典陷阱:循环变量引用
如果闭包直接引用外部函数的循环变量,99%的人都会踩第一个坑!
踩坑演示
假设我们要生成3个函数,分别返回0²、1²、2²:
def bad_create_funcs():
funcs = []
for i in range(3):
# 每次循环定义一个函数,引用当时的i
def bad_func():
return i * i
# 把函数对象存进列表
funcs.append(bad_func)
return funcs
# 取出三个函数
f0, f1, f2 = bad_create_funcs()
# 猜猜输出什么?不是0,1,4!
print(f0(), f1(), f2()) # 输出 4 4 4
为什么全是4?
因为Python的变量是延迟绑定的:
- 循环结束后,外部的
i已经变成了2(最后一次循环的值)
- 闭包
bad_func里存的是i的引用,不是每次循环的副本
- 等我们显式调用
f0/f1/f2时,才会去查i的当前值——全都是2²=4
解决陷阱的两个方案
核心思路都是:让闭包在创建时就“拿到当时的变量副本”,而不是存引用。
方案1:再加一层闭包(函数工厂的函数工厂?)
我们可以用一个「中间辅助函数」,每次循环时把i当参数传进去:
def good_create_funcs():
funcs = []
for i in range(3):
# 定义一个辅助函数,接收当时的x(也就是i的副本)
def make_good_func(x):
# 这个闭包只引用辅助函数里的x,x在每次调用make_good_func时就固定了
def good_func():
return x * x
return good_func
# 立刻调用make_good_func(i),存进去的是绑定了x=i的闭包
funcs.append(make_good_func(i))
return funcs
f0, f1, f2 = good_create_funcs()
print(f0(), f1(), f2()) # 输出 0 1 4 ✔️
方案2:用lambda的默认参数(更简洁的Python写法)
Python函数的默认参数是在函数定义时绑定的,不是调用时!刚好可以利用这个特性:
def lambda_create_funcs():
# 列表推导式里,每次循环给lambda加默认参数x=i
return [lambda x=i: x * x for i in range(3)]
f0, f1, f2 = lambda_create_funcs()
print(f0(), f1(), f2()) # 输出 0 1 4 ✔️
⚠️ 注意:这种写法默认参数只能用不可变类型(int/str/tuple等),如果用可变类型(list/dict),又会有新的默认参数共享陷阱!
闭包要修改外部变量?用nonlocal!
刚才的例子里,闭包只是读取外部变量,但如果我们想修改外部的不可变变量(int/str等),Python会把它当成「内部函数的局部变量」报错。
报错演示
假设我们要做一个“每次调用就+1的独立计数器”:
def bad_counter():
# 外部函数定义的不可变变量count
count = 0
def increment():
# 试图修改count,Python会以为你要定义一个局部变量count
count += 1
return count
return increment
c = bad_counter()
c() # 报错 UnboundLocalError: local variable 'count' referenced before assignment
解决:加nonlocal声明
用nonlocal告诉Python:「这个变量不是局部的,也不是全局的,是外部函数作用域里的!」
def good_counter():
count = 0
def increment():
# 声明count是外层函数的变量
nonlocal count
count += 1
return count
return increment
# 测试独立计数
counter1 = good_counter()
print(counter1(), counter1(), counter1()) # 输出 1 2 3
counter2 = good_counter()
print(counter2(), counter2()) # 输出 1 2 ✔️(和counter1完全独立)
📌 补充:如果外部变量是可变类型(list/dict),不需要nonlocal也能直接修改它的内部元素(比如list.append()),但不能重新赋值整个变量(比如list = [])。
现代Python的简化:3.8+的海象运算符
Python 3.8引入了海象运算符:=,可以把“赋值+返回”写在一行,用来简化这种简单的闭包计数器:
def walrus_counter():
count = 0
# lambda里用海象运算符,每次调用先count+1,再返回新值
return lambda: (count := count + 1)
c = walrus_counter()
print(c(), c(), c()) # 输出 1 2 3 ✔️
闭包的最佳实践
- 循环变量别直接用闭包存引用:要么加中间辅助函数,要么用默认参数绑定不可变副本
- 修改外部不可变变量必须加
nonlocal:可变类型修改内部元素可以不加,但重新赋值整个变量也要加
- 别在闭包里存大对象的引用:闭包会一直保留这些对象的引用,可能导致内存泄漏
- 优先用闭包做「函数工厂」:比如生成不同税率的计算函数、不同阈值的过滤函数,比类更轻量
总结
返回函数是高阶函数的核心用法之一,而闭包是这种用法的「灵魂」:
- 它能记住创建时的外部环境,实现「按需触发的延迟计算」
- 它能封装独立的状态变量,做出轻量的「状态ful」工具
- 它是Python装饰器的底层实现(我们下次再聊装饰器!)
掌握好闭包,你就能写出更优雅、更灵活的Python函数式代码啦~