返回函数

高阶函数的“回退”能力

之前我们聊过高阶函数可以把函数当参数塞进去做逻辑抽象,但高阶函数还有个更有意思的反向特性:把函数当结果吐出来

这种「先存逻辑、后存数据/参数、按需触发」的设计,能帮我们实现延迟计算动态生成相似函数、甚至封装状态变量——这就引出了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

调用方式也变了两步:

  1. 收集参数+生成专属函数对象
  2. 显式调用函数对象才计算
# 第一步:返回的是<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个函数,分别返回

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 ✔️

闭包的最佳实践

  1. 循环变量别直接用闭包存引用:要么加中间辅助函数,要么用默认参数绑定不可变副本
  2. 修改外部不可变变量必须加nonlocal:可变类型修改内部元素可以不加,但重新赋值整个变量也要加
  3. 别在闭包里存大对象的引用:闭包会一直保留这些对象的引用,可能导致内存泄漏
  4. 优先用闭包做「函数工厂」:比如生成不同税率的计算函数、不同阈值的过滤函数,比类更轻量

总结

返回函数是高阶函数的核心用法之一,而闭包是这种用法的「灵魂」:

  • 它能记住创建时的外部环境,实现「按需触发的延迟计算」
  • 它能封装独立的状态变量,做出轻量的「状态ful」工具
  • 它是Python装饰器的底层实现(我们下次再聊装饰器!)

掌握好闭包,你就能写出更优雅、更灵活的Python函数式代码啦~