Skip to content

Python 返回函数与闭包:把"半成品"函数存起来慢慢用

引言:预定蛋糕的两种模式

你去蛋糕店订生日蛋糕,有两种方式:

  • 现买现吃:店员现场做好,你当场拿走;
  • 预定后取:店员记下你的要求(尺寸、口味、装饰),给你一个取货单。三天后你凭取货单来取,蛋糕才新鲜出炉。

Python 的返回函数就是第二种模式——函数不立刻执行,而是返回一个"半成品"函数,等真正需要时才调用它执行。这种"延迟执行"的能力,配合闭包(Closure),是 Python 中最精妙的设计之一。


一、什么是返回函数?

1.1 高阶函数的另一半

上一篇我们学了高阶函数的一种:接收函数作为参数(如 map()sorted())。

高阶函数还有另一种:把函数作为结果返回

python
def lazy_sum(*args):
    def sum():
        ax = 0
        for n in args:
            ax = ax + n
        return ax
    return sum   # 返回的是函数本身,不是计算结果!

关键区别

python
>>> 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:不需要立刻计算

python
# 现在不需要结果,先存起来
f = lazy_sum(1, 2, 3, 4, 5)

# ... 很多行代码后 ...

result = f()   # 现在才真正计算

场景 2:根据条件决定是否计算

python
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 闭包的三大要素

python
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 每次调用都是独立闭包

python
>>> f1 = lazy_sum(1, 3, 5, 7, 9)
>>> f2 = lazy_sum(1, 3, 5, 7, 9)
>>> f1 == f2
False

>>> f1()
25
>>> f2()
25

虽然 f1f2 的参数一样、结果一样,但它们是两个完全独立的闭包,各自保存了自己的 args 副本。

生活化理解:就像你在两家不同的蛋糕店订了同样的蛋糕——虽然订单内容一样,但两张取货单互不相关,各取各的。


三、经典陷阱:循环变量的"幽灵"

这是闭包最容易踩的坑,也是面试必考题。

3.1 问题的出现

python
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

实际结果

python
>>> f1()
9
>>> f2()
9
>>> f3()
9

全是 9! 为什么?

3.2 原因分析:闭包"记住"的是变量,不是值

python
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 解决方案:用参数绑定当前值

python
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
python
>>> f1, f2, f3 = count()
>>> f1()
1
>>> f2()
4
>>> f3()
9

核心技巧f(i) 立刻执行,把 i 的当前值"快照"到 j 里。jf 的局部变量,每次调用 f(i) 都创建新的 j,互不影响。

3.4 一句话避坑指南

返回函数时,不要引用任何循环变量,或者后续会发生变化的变量。如果一定要引用,先用一个中间函数把当前值"锁死"到参数里。


四、nonlocal:让闭包"修改"外部变量

4.1 只读 vs 修改

闭包可以读取外部变量:

python
def inc():
    x = 0
    def fn():
        return x + 1   # 只读取,没问题
    return fn

f = inc()
print(f())   # 1
print(f())   # 1 —— 每次调用都重新读 x,x 永远是 0

但如果想修改外部变量:

python
def inc():
    x = 0
    def fn():
        x = x + 1      # 报错!
        return x
    return fn

f = inc()
print(f())
# UnboundLocalError: local variable 'x' referenced before assignment

4.2 为什么报错?

Python 看到 x = x + 1 里有赋值,就把 x 当成 fn()局部变量。但等号右边的 x 还没初始化,所以报错。

生活化理解:就像你走进一个房间,墙上写着"x = ?",然后有人让你写"x = x + 1"——你不知道 x 是多少,没法算。

4.3 nonlocal 声明:这不是局部变量

python
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())   # 3

nonlocal x 告诉 Python:fn() 里的 x 就是 inc() 里的 x,别当成新变量。

生活化理解nonlocal 就像在房间墙上贴一张纸条——"x 是外面那个 x,不是我新创的"。

4.4 实战:计数器

python
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 配置化的函数工厂

python
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 回调函数与事件处理

python
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 装饰器的基础

python
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 数据封装与私有变量

python
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())       # 1500

balance 被完全封装在闭包里,外部无法直接访问,只能通过提供的三个函数操作——这就是面向对象中"私有变量"的函数式实现。


六、常见误区与避坑指南

误区 1:以为返回函数会立刻执行

python
def lazy():
    print('执行了!')
    return 42

f = lazy   # 没加括号,只是赋值
print('还没执行')
f()        # 这才执行,打印"执行了!"

纠正f = lazy 只是拿到函数,f() 才真正执行。

误区 2:循环中创建闭包引用循环变量

python
# 错误示范
funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])   # [2, 2, 2],不是 [0, 1, 2]

纠正:用默认参数锁定当前值:

python
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

python
def counter():
    n = 0
    def inc():
        n = n + 1   # 报错!
        return n
    return inc

纠正:修改外层变量必须加 nonlocal n

误区 4:闭包导致内存泄漏

python
def heavy():
    big_data = [0] * 1000000   # 大数据
    def fn():
        return len(big_data)
    return fn

f = heavy()
# big_data 一直被 f 引用,无法释放!

纠正:如果闭包引用了大对象,用完记得 del f 解除引用,让垃圾回收器工作。

误区 5:过度使用闭包,代码可读性差

python
# 嵌套太深,难以理解
def a():
    def b():
        def c():
            def d():
                return 42
            return d
        return c
    return b

纠正:嵌套不超过 2 层。如果逻辑复杂,考虑用类(class)替代。


七、动手练习

  1. 基础题:写一个 make_multiplier(n),返回一个函数,该函数把输入乘以 n。测试:double = make_multiplier(2)double(5) 应返回 10
  2. 进阶题:用闭包实现一个"平均数计算器",每次调用传入一个新数,返回当前所有数的平均值。
  3. 挑战题:写一个 once(func) 装饰器,让被装饰的函数只执行一次,第二次及以后调用返回第一次的结果。
  4. 思考题nonlocalglobal 有什么区别?分别用在什么场景?

小结

  • 返回函数:高阶函数把函数作为结果返回,实现"延迟执行"。
  • 闭包(Closure):内部函数引用外部函数的变量,外部函数返回内部函数后,变量被"记住"。
  • 闭包三要素:嵌套函数、引用外部变量、返回内部函数。
  • 循环变量陷阱:闭包引用的是变量本身,不是当时的值。解决方案:用中间函数参数锁定当前值。
  • nonlocal:声明变量不是局部变量,而是外层函数的,才能修改外层变量。
  • 核心心法:闭包是"带状态的函数"——函数不再是无状态的计算工具,而是可以记住信息、封装数据的迷你对象。理解闭包,就理解了 Python 装饰器、回调、函数工厂的设计基础。