函数执行完就什么都没了吗?不,闭包能让它“记住”东西。
想给函数加功能只能改源码吗?不,装饰器就像给英雄穿皮肤,原模原样,但特效全开。

今天的内容是 Python 进阶里的一道小坎,但只要搞懂两个比喻,后面全是套路。
我们开始吧👇


一、闭包:一个会“记仇”的函数

1. 先回忆一下:普通函数的局部变量,用完就扔

你写了一个函数:

def 买东西(钱):
    剩余 = 钱 - 10
    print(f"花了10块,还剩{剩余}")

调用一次,里面的 剩余 变量就出生,执行完就消失。
下次再调用,它又是崭新的,不记得上一次剩多少。

这就像一次性筷子——用完就扔,不留念想。

2. 但有时候,我们想让它“记住”点什么

比如一个计数器:每次调用 +1,并且要记住上次的值。
用全局变量行吗?行,但全局变量谁都能改,不安全。
用类行吗?行,但有点杀鸡用牛刀。

闭包就是专门干这个的:让函数结束后,里面的变量不被销毁,还被内部的小函数一直记着。

3. 闭包的三个条件(缺一不可)

  1. 函数里面套函数(嵌套)

  2. 内层函数用到了外层函数的变量(引用)

  3. 外层函数把内层函数返回出去(返回)

光说太干,上例子(我换一个你肯定记得住的场景:存钱罐

def 存钱罐(初始金额):
    def 投币(硬币):
        nonlocal 初始金额      # 等一下解释这个
        初始金额 += 硬币
        print(f"投了{硬币}元,现在共{初始金额}元")
    return 投币

my_pig = 存钱罐(10)   # 一开始里面有10块
my_pig(5)             # 投5块 → 现在15
my_pig(2)             # 再投2块 → 现在17

你看,每次调用 my_pig,它都记得之前存了多少钱。
这里的 存钱罐 是外层函数,投币 是内层函数,初始金额 就是被“记住”的外层变量。

4. nonlocal 是干什么的?

如果你在内层函数里直接写 初始金额 += 硬币,Python 会以为你想创建一个叫 初始金额 的新局部变量(然后报错,因为没赋值就用了)。
nonlocal 的作用就是告诉 Python:“这个变量不是我内层的,去外层找,我要改它!”

类比:你跟你妈说“我要用咱们家的冰箱”,而不是自己买个小冰箱放屋里。

💡 global 是改全局变量,nonlocal 是改外层函数的变量。两个不一样。


二、装饰器:给函数“穿皮肤”,不改代码加功能

1. 一个真实的需求

你写了一个函数 say_hello(),输出“你好”。
现在老板说:每次调用之前,都要先打印“==========”分隔线。

最笨的办法:改 say_hello 源码,加一行 print
但如果一百个函数都要加呢?或者你根本无权改源码(比如用的是第三方库)?

装饰器就是为这种场景而生的——在不修改原函数代码的情况下,给函数附加新功能。

2. 装饰器本质就是一个特殊闭包

它的条件也是在闭包三条件上多加一条:外层函数接收的是一个函数

先看一个最简单的装饰器(不用 @ 符号的原始写法):

def 加分隔线(原函数):
    def 包装函数():
        print("==========")    # 新加的功能
        原函数()               # 调用原来的函数
    return 包装函数

def 打招呼():
    print("你好")

打招呼 = 加分隔线(打招呼)   # 手工“装饰”
打招呼()

输出:

==========
你好

你看,打招呼 这个名字现在指向的不再是原来的函数,而是被 包装函数 替换了。
这就是装饰器的本质:用一个新函数把原函数包起来,然后替换掉原函数

3. @ 语法糖:让代码更甜

上面那行 打招呼 = 加分隔线(打招呼) 写起来有点麻烦。
Python 提供了 @ 符号,直接放在函数定义上面:

@加分隔线
def 打招呼():
    print("你好")

完全等价。解释器看到 @加分隔线,就会自动执行 打招呼 = 加分隔线(打招呼)

4. 如果原函数有参数和返回值怎么办?

大部分人在这里会晕。记住一个万能模板就行:

def 装饰器(原函数):
    def 包装函数(*args, **kwargs):
        # 这里写你要添加的功能
        print("执行前做点啥")
        结果 = 原函数(*args, **kwargs)   # 调用原函数,传所有参数
        # 这里也可以写执行后的功能
        print("执行后做点啥")
        return 结果                     # 把原函数的返回值原样返回
    return 包装函数
  • *args 收集所有位置参数

  • **kwargs 收集所有关键字参数

  • 原函数的返回值通过一个变量保存,最后再 return 出去

这样不管原函数长什么样(没参数、有参数、有返回值),你的装饰器都能适配。


三、多个装饰器:穿衣服的顺序

假设你有两个装饰器:一个加分隔线,一个加时间戳。

@加分隔线
@加时间戳
def 发动态():
    print("今天天气真好")

执行顺序是怎样的?记住两个口诀:

  • 装饰的时候:从下往上(离函数最近的先装饰)

  • 执行的时候:从上往下(先执行最外层的装饰器逻辑,再往里)

具体到这个例子:

  1. @加时间戳 先作用,把 发动态 包成“带时间戳版本”

  2. @加分隔线 再作用,把上面那个结果再包成“带分隔线+时间戳版本”

调用 发动态() 时:
先打印分隔线 → 然后打印时间戳 → 最后执行真正的 print("今天天气真好")

就像穿衣服:先穿内衣(加时间戳),再穿外套(加分隔线)。
出门时别人先看到外套,脱掉外套才看到内衣。


四、带参数的装饰器:给装饰器传“配置”

有时候你希望同一个装饰器能根据不同的参数做出不同行为。
比如:@repeat(3) 表示重复执行3次,@repeat(5) 重复5次。

这需要三层嵌套(普通装饰器是两层):

def repeat(times):               # 第一层:接收参数
    def decorator(func):         # 第二层:接收原函数
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(3)
def say_hi():
    print("嗨")

执行流程:

  • repeat(3) 先执行,返回 decorator

  • @decorator 再执行,等价于 say_hi = decorator(say_hi)

所以带参数的装饰器 = 函数工厂:你给参数,它给你生成一个真正的装饰器。


五、闭包 vs 装饰器:别搞混

对比 闭包 装饰器
核心目的 记住外层变量,延长生命周期 不修改原函数,增加功能
外层参数 可以是任何数据(数字、字符串等) 通常是一个函数(被装饰的目标)
必须返回 内层函数 内层函数(包装后的新函数)
关系 闭包是一种技术 装饰器是闭包的一种应用

所有装饰器都是闭包,但不是所有闭包都是装饰器。


六、核心结论

  1. 闭包三条件:有嵌套、有引用、有返回。

  2. 要修改外层函数变量 → 用 nonlocal

  3. 装饰器本质 = 接收函数、返回新函数的闭包。

  4. @deco 等价于 func = deco(func)

  5. 万能装饰器模板:def deco(func): def wrapper(*args, **kwargs): ... return result

  6. 多个装饰器:离函数最近的先装饰,执行时外层先执行。

  7. 带参数的装饰器 = 三层嵌套函数(参数工厂层、装饰器层、包装层)。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐