AI大模型学习03:闭包和装饰器
函数执行完就什么都没了吗?不,闭包能让它“记住”东西。
想给函数加功能只能改源码吗?不,装饰器就像给英雄穿皮肤,原模原样,但特效全开。
今天的内容是 Python 进阶里的一道小坎,但只要搞懂两个比喻,后面全是套路。
我们开始吧👇
一、闭包:一个会“记仇”的函数
1. 先回忆一下:普通函数的局部变量,用完就扔
你写了一个函数:
def 买东西(钱):
剩余 = 钱 - 10
print(f"花了10块,还剩{剩余}")
调用一次,里面的 剩余 变量就出生,执行完就消失。
下次再调用,它又是崭新的,不记得上一次剩多少。
这就像一次性筷子——用完就扔,不留念想。
2. 但有时候,我们想让它“记住”点什么
比如一个计数器:每次调用 +1,并且要记住上次的值。
用全局变量行吗?行,但全局变量谁都能改,不安全。
用类行吗?行,但有点杀鸡用牛刀。
闭包就是专门干这个的:让函数结束后,里面的变量不被销毁,还被内部的小函数一直记着。
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("今天天气真好")
执行顺序是怎样的?记住两个口诀:
-
装饰的时候:从下往上(离函数最近的先装饰)
-
执行的时候:从上往下(先执行最外层的装饰器逻辑,再往里)
具体到这个例子:
-
@加时间戳先作用,把发动态包成“带时间戳版本” -
@加分隔线再作用,把上面那个结果再包成“带分隔线+时间戳版本”
调用 发动态() 时:
先打印分隔线 → 然后打印时间戳 → 最后执行真正的 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 装饰器:别搞混
| 对比 | 闭包 | 装饰器 |
|---|---|---|
| 核心目的 | 记住外层变量,延长生命周期 | 不修改原函数,增加功能 |
| 外层参数 | 可以是任何数据(数字、字符串等) | 通常是一个函数(被装饰的目标) |
| 必须返回 | 内层函数 | 内层函数(包装后的新函数) |
| 关系 | 闭包是一种技术 | 装饰器是闭包的一种应用 |
所有装饰器都是闭包,但不是所有闭包都是装饰器。
六、核心结论
-
闭包三条件:有嵌套、有引用、有返回。
-
要修改外层函数变量 → 用
nonlocal。 -
装饰器本质 = 接收函数、返回新函数的闭包。
-
@deco等价于func = deco(func)。 -
万能装饰器模板:
def deco(func): def wrapper(*args, **kwargs): ... return result。 -
多个装饰器:离函数最近的先装饰,执行时外层先执行。
-
带参数的装饰器 = 三层嵌套函数(参数工厂层、装饰器层、包装层)。
更多推荐

所有评论(0)