Python 返回函数与闭包:把"半成品"函数存起来慢慢用
引言:预定蛋糕的两种模式
你去蛋糕店订生日蛋糕,有两种方式:
- 现买现吃:店员现场做好,你当场拿走;
- 预定后取:店员记下你的要求(尺寸、口味、装饰),给你一个取货单。三天后你凭取货单来取,蛋糕才新鲜出炉。
Python 的返回函数就是第二种模式——函数不立刻执行,而是返回一个"半成品"函数,等真正需要时才调用它执行。这种"延迟执行"的能力,配合闭包(Closure),是 Python 中最精妙的设计之一。
一、什么是返回函数?
1.1 高阶函数的另一半
上一篇我们学了高阶函数的一种:接收函数作为参数(如 map()、sorted())。
高阶函数还有另一种:把函数作为结果返回。
def lazy_sum(*args):
def sum():
ax = 0
for n in args:
ax = ax + n
return ax
return sum # 返回的是函数本身,不是计算结果!关键区别:
>>> f = lazy_sum(1, 3, 5, 7, 9)
>>> f
<function lazy_sum.<locals>.sum at 0x101c6ed90> # f 是一个函数!
>>> f() # 真正调用,才计算结果
25生活化理解:lazy_sum 像蛋糕店接单员——你告诉它要什么(参数),它给你一张取货单(返回的函数)。三天后你拿着取货单来(调用 f()),才真正开始做蛋糕。
1.2 为什么需要延迟执行?
场景 1:不需要立刻计算
# 现在不需要结果,先存起来
f = lazy_sum(1, 2, 3, 4, 5)
# ... 很多行代码后 ...
result = f() # 现在才真正计算场景 2:根据条件决定是否计算
def get_calculator(operation):
if operation == 'sum':
def calc(*args):
return sum(args)
elif operation == 'product':
def calc(*args):
result = 1
for x in args:
result *= x
return result
return calc
calc = get_calculator('sum') # 先拿到函数
if user_confirmed:
print(calc(1, 2, 3)) # 确认后才执行二、闭包:函数的"记忆"能力
2.1 什么是闭包
仔细观察 lazy_sum 的例子:内部函数 sum 引用了外部函数 lazy_sum 的参数 args。
当 lazy_sum 返回 sum 时,args 的值被"记住"了,保存在返回的函数中。这种"函数 + 它引用的外部环境"的组合,就叫闭包(Closure)。
生活化理解:闭包就像一个带备忘便签的函数——便签上写着"别忘了这些变量",函数走到哪里都带着它。
2.2 闭包的三大要素
def outer(x): # 1. 外部函数
y = x * 2 # 2. 外部函数的局部变量
def inner(): # 3. 内部函数(闭包的核心)
return x + y # 引用了外部变量
return inner # 返回内部函数
f = outer(10) # x=10, y=20 被"记住"了
print(f()) # 30 —— x 和 y 还在!| 要素 | 说明 | 类比 |
|---|---|---|
| 嵌套函数 | 函数内部定义函数 | 大盒子里套小盒子 |
| 引用外部变量 | 内部函数用外部函数的变量 | 小盒子用了大盒子的东西 |
| 返回内部函数 | 外部函数把内部函数"送出去" | 大盒子把小盒子寄给别人 |
2.3 每次调用都是独立闭包
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1 == f2
False
>>> f1()
25
>>> f2()
25虽然 f1 和 f2 的参数一样、结果一样,但它们是两个完全独立的闭包,各自保存了自己的 args 副本。
生活化理解:就像你在两家不同的蛋糕店订了同样的蛋糕——虽然订单内容一样,但两张取货单互不相关,各取各的。
三、经典陷阱:循环变量的"幽灵"
这是闭包最容易踩的坑,也是面试必考题。
3.1 问题的出现
def count():
fs = []
for i in range(1, 4):
def f():
return i * i
fs.append(f)
return fs
f1, f2, f3 = count()预期结果:f1()=1, f2()=4, f3()=9
实际结果:
>>> f1()
9
>>> f2()
9
>>> f3()
9全是 9! 为什么?
3.2 原因分析:闭包"记住"的是变量,不是值
for i in range(1, 4):
def f():
return i * i # f 引用的是变量 i,不是 i 的当前值!
fs.append(f)当 count() 执行完,循环变量 i 已经变成了 3。三个 f 函数都引用同一个变量 i,所以调用时都拿到 3,结果都是 9。
生活化理解:就像三个同学约定"明天早上告诉我现在几点"——但"现在"是变量,等他们明天来问你时,"现在"已经是明天了,所以三个人听到的都是同一个时间。
3.3 解决方案:用参数绑定当前值
def count():
def f(j): # 新增一层函数,用参数 j 固定当前值
def g():
return j * j
return g
fs = []
for i in range(1, 4):
fs.append(f(i)) # f(i) 立刻执行,i 的当前值被传入并绑定到 j
return fs>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9核心技巧:f(i) 立刻执行,把 i 的当前值"快照"到 j 里。j 是 f 的局部变量,每次调用 f(i) 都创建新的 j,互不影响。
3.4 一句话避坑指南
返回函数时,不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用,先用一个中间函数把当前值"锁死"到参数里。
四、nonlocal:让闭包"修改"外部变量
4.1 只读 vs 修改
闭包可以读取外部变量:
def inc():
x = 0
def fn():
return x + 1 # 只读取,没问题
return fn
f = inc()
print(f()) # 1
print(f()) # 1 —— 每次调用都重新读 x,x 永远是 0但如果想修改外部变量:
def inc():
x = 0
def fn():
x = x + 1 # 报错!
return x
return fn
f = inc()
print(f())
# UnboundLocalError: local variable 'x' referenced before assignment4.2 为什么报错?
Python 看到 x = x + 1 里有赋值,就把 x 当成 fn() 的局部变量。但等号右边的 x 还没初始化,所以报错。
生活化理解:就像你走进一个房间,墙上写着"x = ?",然后有人让你写"x = x + 1"——你不知道 x 是多少,没法算。
4.3 nonlocal 声明:这不是局部变量
def inc():
x = 0
def fn():
nonlocal x # 声明:x 不是局部变量,是外层函数的
x = x + 1
return x
return fn
f = inc()
print(f()) # 1
print(f()) # 2
print(f()) # 3nonlocal x 告诉 Python:fn() 里的 x 就是 inc() 里的 x,别当成新变量。
生活化理解:nonlocal 就像在房间墙上贴一张纸条——"x 是外面那个 x,不是我新创的"。
4.4 实战:计数器
def create_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c1 = create_counter()
print(c1(), c1(), c1()) # 1, 2, 3
c2 = create_counter()
print(c2(), c2()) # 1, 2 —— 独立计数器,互不影响应用场景:生成唯一 ID、记录函数调用次数、实现简易缓存等。
五、实战应用场景
5.1 配置化的函数工厂
def power_factory(exponent):
"""生成指定指数的幂函数"""
def power(x):
return x ** exponent
return power
square = power_factory(2) # 平方函数
cube = power_factory(3) # 立方函数
print(square(5)) # 25
print(cube(5)) # 125生活化理解:函数工厂像模具——传入参数做出模具,以后每次用模具压出产品。
5.2 回调函数与事件处理
def on_button_click(button_name):
def handler():
print(f'{button_name} 被点击了!')
# 执行具体逻辑...
return handler
# 注册回调,但不立刻执行
btn1_handler = on_button_click('保存按钮')
btn2_handler = on_button_click('取消按钮')
# 用户点击时才执行
btn1_handler() # 保存按钮 被点击了!5.3 装饰器的基础
def log(func):
def wrapper(*args, **kwargs):
print(f'调用 {func.__name__}()')
return func(*args, **kwargs)
return wrapper
@log
def hello():
print('Hello!')
hello()
# 输出:
# 调用 hello()
# Hello!装饰器本质上就是返回函数的高阶函数,@log 等价于 hello = log(hello)。
5.4 数据封装与私有变量
def create_account(balance):
def deposit(amount):
nonlocal balance
balance += amount
return balance
def withdraw(amount):
nonlocal balance
if amount > balance:
return '余额不足'
balance -= amount
return balance
def get_balance():
return balance
return deposit, withdraw, get_balance
dep, wit, bal = create_account(1000)
print(dep(500)) # 1500
print(wit(2000)) # 余额不足
print(bal()) # 1500balance 被完全封装在闭包里,外部无法直接访问,只能通过提供的三个函数操作——这就是面向对象中"私有变量"的函数式实现。
六、常见误区与避坑指南
误区 1:以为返回函数会立刻执行
def lazy():
print('执行了!')
return 42
f = lazy # 没加括号,只是赋值
print('还没执行')
f() # 这才执行,打印"执行了!"纠正:f = lazy 只是拿到函数,f() 才真正执行。
误区 2:循环中创建闭包引用循环变量
# 错误示范
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # [2, 2, 2],不是 [0, 1, 2]纠正:用默认参数锁定当前值:
funcs = []
for i in range(3):
funcs.append(lambda i=i: i) # 关键:i=i 绑定当前值
print([f() for f in funcs]) # [0, 1, 2]误区 3:忘记 nonlocal 导致 UnboundLocalError
def counter():
n = 0
def inc():
n = n + 1 # 报错!
return n
return inc纠正:修改外层变量必须加 nonlocal n。
误区 4:闭包导致内存泄漏
def heavy():
big_data = [0] * 1000000 # 大数据
def fn():
return len(big_data)
return fn
f = heavy()
# big_data 一直被 f 引用,无法释放!纠正:如果闭包引用了大对象,用完记得 del f 解除引用,让垃圾回收器工作。
误区 5:过度使用闭包,代码可读性差
# 嵌套太深,难以理解
def a():
def b():
def c():
def d():
return 42
return d
return c
return b纠正:嵌套不超过 2 层。如果逻辑复杂,考虑用类(class)替代。
七、动手练习
- 基础题:写一个
make_multiplier(n),返回一个函数,该函数把输入乘以 n。测试:double = make_multiplier(2),double(5)应返回10。 - 进阶题:用闭包实现一个"平均数计算器",每次调用传入一个新数,返回当前所有数的平均值。
- 挑战题:写一个
once(func)装饰器,让被装饰的函数只执行一次,第二次及以后调用返回第一次的结果。 - 思考题:
nonlocal和global有什么区别?分别用在什么场景?
小结
- 返回函数:高阶函数把函数作为结果返回,实现"延迟执行"。
- 闭包(Closure):内部函数引用外部函数的变量,外部函数返回内部函数后,变量被"记住"。
- 闭包三要素:嵌套函数、引用外部变量、返回内部函数。
- 循环变量陷阱:闭包引用的是变量本身,不是当时的值。解决方案:用中间函数参数锁定当前值。
- nonlocal:声明变量不是局部变量,而是外层函数的,才能修改外层变量。
- 核心心法:闭包是"带状态的函数"——函数不再是无状态的计算工具,而是可以记住信息、封装数据的迷你对象。理解闭包,就理解了 Python 装饰器、回调、函数工厂的设计基础。