前置认知:函数也是个"东西"
在 Python 里,函数和数字、字符串一样,是一个可以传来传去的对象。你可以把函数存进变量、当作参数传给另一个函数、甚至让一个函数返回另一个函数。
这个认知是理解装饰器的地基。如果觉得抽象,可以这样理解:
把函数想象成一道菜谱 。菜谱本身不是菜,它只是一张写满步骤的纸。你可以把这张纸递给别人(传参),也可以用一张新菜谱替换掉旧菜谱(赋值)。装饰器做的事情,就是在不改写原菜谱的前提下,给这道菜加一层摆盘。
一旦接受了"函数是个能传来传去的东西",后面的一切都是水到渠成。
第一步:手动包装函数
最朴素的思路:把函数当作参数传进去,在调用前后加上额外逻辑。
python
def fun():
print("我是原函数")
def wrapper1(func):
print("装饰前置逻辑")
func() # 调用原函数
print("装饰后置逻辑")
wrapper1(fun)
运行输出:
装饰前置逻辑
我是原函数
装饰后置逻辑
wrapper1 接收了 fun,先打印前置逻辑,再替你调用 fun(),最后打印后置逻辑。原函数被完整地"包"在了中间。
但这个写法有个问题:fun 还是原来的 fun,每次想用包装版都得手动写 wrapper1(fun)。能不能让 fun 本身就"升级"成包装后的版本?
第二步:函数可以重新赋值
函数名本质上就是一个指向函数对象的变量名,所以可以重新赋值。
python
def wrapper2(func):
print("装饰前置逻辑")
return func # 不加括号()不调用,直接返回函数对象本身
fun = wrapper2(fun) # 将自身作为参数传递,返回并赋值给fun
运行输出:
装饰前置逻辑
注意:这里没有输出 "我是原函数"。因为 wrapper2() 没有调用原函数,只是返回了原函数的引用。
赋值之后,fun 变成了 wrapper2() 的返回值------也就是原函数本身。所以执行 fun() 仍然可以调用原函数,没丢。
但这仍然不够好:前置逻辑在赋值时就跑了,而且原函数没被调用。我们想要的是------赋值时不执行任何东西,等真正调用 fun() 时才执行包装逻辑。
第三步:返回一个全新的函数
核心技巧:在 wrapper 里面再套一层函数,外层函数直接返回内层函数。内层函数定义时不会执行,只有被调用时才执行。
python
def decorator(func):
def wrapper():
print("装饰前置逻辑")
func() # 调用原函数
print("装饰后置逻辑")
return wrapper # 返回wrapper函数对象,不调用它
fun = decorator(fun) # 赋值新函数wrapper给fun,此时wrapper内容没被调用
fun() # 此时fun就是全新的函数了,调用fun()就是调用wrapper()
运行输出:
装饰前置逻辑
我是原函数
装饰后置逻辑
这一步要分清两个阶段:
- 赋值时(只造壳,不执行) :
fun = decorator(fun)调用了decorator(),把原fun传进去。decorator内部定义了wrapper但没有调用它,只是把wrapper这个函数对象返回出来,赋给了fun``。此时wrapper` 里的代码一行都没跑。 - 调用时(才执行包装逻辑) :
fun()此时调用的已经不是原函数了,而是wrapper()。wrapper执行前置逻辑 → 调用原函数 → 执行后置逻辑。完整的包装效果在这一刻才显现。
外层函数负责"注册"(接收原函数、返回新函数),内层函数负责"干活"(前置 + 原函数 + 后置)。这就是装饰器的标准结构。
三个必须想通的关键问题
问题 1:为什么参数放在最外层?
有人可能会想:能不能把 func 参数放到内层 wrapper 上?试一下:
python
def decorator():
def wrapper(func):
print("装饰前置逻辑")
func()
print("装饰后置逻辑")
return func
return wrapper
fun = decorator()
fun(fun) # 会报错!
fun = decorator() 返回了 wrapper 函数,赋值给 fun。但此时原函数 fun 已经被 wrapper 覆盖而丢失了 ------变量名 fun 现在指向的是 wrapper,不再是原来的那个函数。
调用 fun(fun) 时,左边的 fun 是 wrapper(需要一个参数 func),右边的 fun 也是 wrapper(不是原函数)。类型对不上,直接报错。
结论:必须把参数放在最外层,这样原函数在赋值替换之前就被"捕获"进闭包了,不会因为变量名被覆盖而丢失。
问题 2:原函数有参数怎么办?
前面的例子原函数都没有参数。但真实业务函数几乎都有参数,比如 add(a, b)。答案是用 *args, **kwargs------Python 的"万能参数容器",可以接收任意数量、任意形式的参数:
python
def log_decorator(func):
def inner(*args, **kwargs):
print(f"【日志】准备调用函数 {func.__name__}")
res = func(*args, **kwargs) # 把参数传给原函数,接收返回值
print(f"【日志】函数执行完毕")
return res # 必须把原函数返回值返回出去!很容易漏
return inner
def add(a, b):
return a + b
add = log_decorator(add)
print(add(3, 5))
运行输出:
csharp
【日志】准备调用函数 add
【日志】函数执行完毕
8
⚠️ 易错点 :
return res这一行非常容易漏。如果原函数有返回值而inner没有把它返回出去,调用方拿到的就是None,bug 非常隐蔽。记住:装饰器是"透明管道",原函数返回什么,你就原样吐回去。
问题 3:装饰器到底解决了什么问题?
本质: 装饰器是利用闭包 + 函数对象赋值,在不修改原函数源代码、不修改原函数调用方式的前提下,给已有函数动态增加额外功能的工具。
它实际解决的痛点:
| 痛点 | 说明 |
|---|---|
| 重复代码冗余 | 需要给多个函数加相同附加逻辑(如日志、计时、权限校验),不用每个函数都抄一遍 |
| 侵入业务代码 | 不想改原有业务代码(开闭原则),只在外围包裹附加逻辑,原函数一行不动 |
| 职责混杂 | 功能解耦,职责拆分,各司其职,代码可读性和可维护性大幅提升 |
| 无法灵活扩展 | 可以灵活叠加多个装饰器、按需开关功能,像搭积木一样组合 |
@ 语法糖:一行代码背后藏着什么
前面我们一直手写 fun = decorator(fun) 这行赋值。Python 觉得这行太常用了,就发明了 @ 符号来替你干这件事。两种写法完全等价:
python
def outer(func):
def inner():
print("前置")
func()
print("后置")
return inner
# 写法1:原始手写赋值
def hello():
print("原函数")
hello = outer(hello) # 这一行就是 @ 替你干的事
# 写法2:@语法糖简写,和上面完全等价
@outer
def hello():
print("原函数")
@outer 就是一个快捷方式,它在定义完 hello 之后,自动执行 hello = outer(hello)。仅此而已。但就是这个"仅此而已",给新手挖了不少坑。
坑 1:隐藏了赋值替换动作
手写 hello = outer(hello),你能清晰看到变量 hello 被覆盖,指向了返回的 inner。但 @outer 一行搞定,看不到变量替换,大脑很难反应:hello 已经不是原来的函数了。
💡 破解方法 :每次看到
@xxx,脑子里自动翻译成函数名 = xxx(函数名)。
坑 2:多层带参装饰器嵌套混乱
python
def log(level):
def deco(func):
def inner():
pass
return inner
return deco
@log("INFO")
def test():
pass
展开原始代码是:
python
temp = log("INFO")
test = temp(test)
@log("INFO") 实际上是两步:先执行 log("INFO") 得到一个装饰器,再用这个装饰器去装饰 test。中间临时变量被隐藏,链条断裂,越看越乱。
坑 3:叠加装饰器执行顺序反直觉
python
@A
@B
def f():
pass
展开后是:
python
# 先执行内层 B 包裹 f
f = B(f)
# 再用 A 包裹上一步的结果
f = A(f)
真实代码:f = A(B(f))。书写从上到下,执行从下到上。
记忆口诀:装饰从下往上包,执行从外往里穿。
坑 4:被装饰函数元信息丢失
装饰之后,add.__name__ 不再是 "add",变成了 "inner"。在调试、日志、文档生成时会出问题。解决:functools.wraps(func),生产代码必加。
python
import functools
def log_decorator(func):
@functools.wraps(func) # 保留原函数的名字、文档注释
def inner(*args, **kwargs):
print("日志")
return func(*args, **kwargs)
return inner
@log_decorator
def add(a, b):
"""两数相加"""
return a + b
print(add.__name__) # add,而不是 inner
print(add.__doc__) # 两数相加
functools.wraps 到底干了什么
@functools.wraps(func) 本质上是把原函数的元信息复制到 inner 上。手写一个简化版就能看清楚核心逻辑:
python
def my_wraps(func):
def decorator(wrapper):
wrapper.__name__ = func.__name__
wrapper.__doc__ = func.__doc__
wrapper.__module__ = func.__module__
wrapper.__qualname__ = func.__qualname__
wrapper.__annotations__ = func.__annotations__
return wrapper
return decorator
💡 上面是简化版,官方
functools.wraps还会更新__dict__并设置__wrapped__属性,功能更完整。这里为了理解核心原理做了精简。
展开过程:
temp_decorator = my_wraps(func)------my_wraps的形参func= 外面收到的原始函数,返回里面的decorator函数inner = temp_decorator(inner)------inner传递给参数wrapper,执行后inner的元信息被替换成原函数的
装饰器进阶
带参数的装饰器
如果装饰器本身需要传参(比如日志级别),要多套一层函数。
先看错误写法:
python
def decorator(func, level): # 两个参数
@functools.wraps(func)
def inner(*args, **kwargs):
print(f"[{level}] 执行函数 {func.__name__}")
return func(*args, **kwargs)
return inner
@decorator("INFO") # 报错!@只会把函数传给第一个形参func,level没人接收
def test():
pass
@ 语法糖只能自动传一个参数------被装饰的函数 test。@decorator("INFO") 会先把 "INFO" 传给第一个参数 func,level 收不到值,直接报错。
正确写法:在外面再包一层,专门接收装饰器自身的参数:
python
import functools
def log(level):
# 外层接收装饰器参数 level
def decorator(func):
@functools.wraps(func)
def inner(*args, **kwargs):
print(f"[{level}] 执行函数 {func.__name__}")
return func(*args, **kwargs)
return inner
return decorator
@log("INFO")
def test():
print("业务逻辑")
test()
运行输出:
bash
[INFO] 执行函数 test
业务逻辑
执行流程:
temp_decorator = log("INFO")------level = "INFO"被闭包保存,temp_decorator就是内部的decorator函数test = temp_decorator(test)------decorator的形参func= 原始test函数,返回inner赋给test- 调用
test()→ 执行inner------inner既能拿到外层的level,又能拿到上一层的func,这就是闭包的威力
坑 5:@ 只能自动传一个参数
这就是上面错误写法的原因。@ 语法糖只负责把被装饰函数传给装饰器,如果装饰器还需要额外参数,就必须用"再套一层"的方式让参数在 @ 之前就通过闭包传入。
多个装饰器叠加
python
def dec1(func):
def inner(*args, **kwargs):
print("dec1 before")
r = func(*args, **kwargs)
print("dec1 after")
return r
return inner
def dec2(func):
def inner(*args, **kwargs):
print("dec2 before")
r = func(*args, **kwargs)
print("dec2 after")
return r
return inner
@dec1
@dec2
def f():
print("f run")
f()
运行输出:
erlang
dec1 before
dec2 before
f run
dec2 after
dec1 after
展开过程:
python
# 先执行内层 dec2 包裹 f
f = dec2(f)
# 再用 dec1 包裹上一步的结果
f = dec1(f)
调用时像剥洋葱:最外层 dec1 先打印 before,进入 dec2 打印 before,执行原函数 f,然后 dec2 打印 after,最后 dec1 打印 after。
完整口诀:装饰从下往上包,执行从外往里穿,返回从里往外穿。
补充:类装饰器
除了用函数写装饰器,还可以用类。原理一样:只要类实现了 __call__ 方法,它的实例就能像函数一样被调用。
python
class LogDecorator:
def __init__(self, func):
self.func = func # 接收被装饰的函数
def __call__(self, *args, **kwargs):
print(f"【日志】准备调用 {self.func.__name__}")
res = self.func(*args, **kwargs)
print(f"【日志】执行完毕")
return res
@LogDecorator
def greet(name):
print(f"你好,{name}")
return "done"
greet("小明")
运行输出:
【日志】准备调用 greet
你好,小明
【日志】执行完毕
@LogDecorator 等价于 greet = LogDecorator(greet)。调用 greet("小明") 时,Python 自动触发 __call__ 方法。类装饰器的好处是可以保存状态(比如统计调用次数),在需要维护内部状态的场景下比函数装饰器更方便。
实战:两个真实场景
场景一:函数执行计时器
python
import time
import functools
def timer(func):
@functools.wraps(func)
def inner(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 耗时: {end - start:.4f}秒")
return result
return inner
@timer
def process_data():
time.sleep(1.5)
return "处理完成"
process_data()
# 输出: process_data 耗时: 1.5012秒
给任何函数加上 @timer,就能自动打印执行耗时,排查性能瓶颈时非常有用。
场景二:登录权限校验
python
import functools
# current_user 来自 Flask-Login,此处省略导入和初始化
# 在真实项目中它是一个全局代理对象,表示当前登录用户
def login_required(func):
@functools.wraps(func)
def inner(*args, **kwargs):
if not current_user.is_authenticated:
return "请先登录"
return func(*args, **kwargs)
return inner
@login_required
def view_profile():
return "这是你的个人主页"
@login_required
def edit_profile():
return "编辑你的资料"
⚠️ 上面的
current_user是 Flask-Login 提供的全局代理对象,直接复制运行会报NameError。这里为了聚焦装饰器逻辑省略了框架部分,真实项目中由框架自动注入。
多个接口需要登录校验,不用每个函数里都写 if not logged_in,一个 @login_required 搞定。Flask、Django 的权限装饰器就是这个原理。
一句话总结
把整个推导链串起来:
函数是对象 → 可以当参数传 → 可以被赋值替换 → 外层函数接收原函数、内层函数包裹逻辑 → 返回新函数覆盖旧名字 →
@语法糖替你写这行赋值。