返回函数
Python 中的函数也是对象,可以像数字或字符串一样被保存、传递和返回。一个函数内部创建并返回另一个函数,就是“返回函数”。这种写法把“准备规则”和“执行规则”分开,是高阶函数、闭包和装饰器的基础。
本文使用 Python 3.10 及以上版本。读完后,你应该能够区分返回函数与立即调用函数,理解闭包如何保存外部变量,并知道何时用函数工厂封装配置。
返回函数与返回调用结果
返回函数名时,返回的是函数对象;加上括号则会立即调用函数,返回调用结果:
def double(number):
return number * 2
def choose_operation():
return double
operation = choose_operation()
print(operation(5)) # 10
上例中的 choose_operation() 返回 double,所以 operation 仍然可以被调用。下面的写法则返回整数 10,调用者无法再把结果当成函数使用:
def call_double():
return double(5)
result = call_double()
print(result) # 10
判断一个值是否可调用,可以使用内置函数 callable():
print(callable(operation)) # True
print(callable(result)) # False
最简单的函数工厂
如果一组函数只有某个配置不同,可以写一个函数工厂:工厂接收配置,返回一个已经准备好的函数。
def make_multiplier(factor):
def multiply(number):
return number * factor
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(4)) # 8
print(triple(4)) # 12
调用 make_multiplier(2) 时,内部函数 multiply 被创建并返回。之后调用 double(4),才会把 4 乘以保存的 factor。
函数工厂适合把重复的配置集中到创建阶段,例如税率、日志前缀、权限范围或单位换算规则:
def make_price_converter(rate):
def convert(price):
return round(price * rate, 2)
return convert
to_usd = make_price_converter(0.14)
print(to_usd(100)) # 14.0
闭包:返回函数如何记住变量
内部函数引用了外部函数的局部变量,并且外部函数已经返回时,这种函数和它引用的环境一起称为闭包。double 不仅是 multiply 的代码,还携带了创建它时的 factor=2。
可以通过函数的 __closure__ 观察被捕获的单元,但业务代码不应依赖这个实现细节:
double = make_multiplier(2)
print(double.__closure__[0].cell_contents) # 2
闭包捕获的是变量绑定,而不是创建时变量的一个普通副本。外部变量如果后来被重新绑定,闭包读取到的可能是新值。要构建一组闭包时,常见的晚绑定问题如下:
def make_operations():
operations = []
for factor in [1, 2, 3]:
operations.append(lambda number: number * factor)
return operations
operations = make_operations()
print([operation(10) for operation in operations]) # [30, 30, 30]
三个 lambda 共享同一个循环变量 factor,真正调用时它已经是 3。用默认参数在每轮固定当前值即可:
def make_operations():
operations = []
for factor in [1, 2, 3]:
operations.append(lambda number, factor=factor: number * factor)
return operations
print([operation(10) for operation in make_operations()]) # [10, 20, 30]
使用 nonlocal 修改闭包状态
闭包默认只能读取外部变量。若要在内部函数中重新绑定外部函数的局部变量,需要使用 nonlocal:
def make_counter(start=0):
count = start
def next_count():
nonlocal count
count += 1
return count
return next_count
counter = make_counter(10)
print(counter()) # 11
print(counter()) # 12
nonlocal 只能找到外层函数作用域中的变量,不能修改全局变量;修改全局变量要使用 global,但通常应优先显式传递状态。闭包状态只适合简单、局部的场景;状态复杂时,类往往更容易阅读和测试:
class Counter:
def __init__(self, start=0):
self.count = start
def next(self):
self.count += 1
return self.count
类型提示返回的函数
使用 collections.abc.Callable 可以说明函数工厂接收和返回的函数签名:
from collections.abc import Callable
def make_multiplier(factor: int) -> Callable[[int], int]:
def multiply(number: int) -> int:
return number * factor
return multiply
Callable[[int], int] 表示“接收一个整数并返回一个整数的可调用对象”。如果函数参数或返回值很多,可以为签名定义类型别名,避免重复书写。
常见错误与选择建议
把函数调用结果当成函数
def make_greeting(name):
return lambda: f"你好,{name}"
greeting = make_greeting("小林")
print(greeting()) # 你好,小林
greeting = make_greeting("小林") 是创建闭包;只有 greeting() 才是执行它。若写成 return lambda(): ...,语法上也是错误的,因为 lambda 的参数列表不能写调用括号。
返回过于复杂的匿名函数
闭包适合短小、清晰的规则。逻辑包含多个分支、异常处理或需要独立测试时,应使用有名称的内部函数,甚至改用类。
忽略可变对象捕获
闭包捕获列表或字典时,多个返回函数可能共享同一个可变对象。需要独立状态时,在工厂函数内部创建对象,而不是把外部对象直接捕获。
练习
- 编写
make_greater_than(limit),返回判断数字是否大于limit的函数。 - 使用闭包实现一个带步长的计数器,并提供重置功能。
- 构造三个单位转换函数,修复循环中
lambda的晚绑定问题。 - 比较闭包计数器和
Counter类的可读性与测试方式。
小结
- 返回函数时传递函数对象,写括号则会立即执行函数。
- 函数工厂可以把配置保存到闭包中,创建出行为不同的函数。
- 闭包会记住外层变量;循环中创建闭包时要注意变量的晚绑定。
- 使用
nonlocal可以修改外层函数的局部变量,但复杂状态通常更适合用类表示。 Callable类型提示能清楚表达函数工厂的输入和输出签名。