如果你写过一段时间Python,大概率见过这样的代码------函数定义上方悬着一个@something,看着像个符咒。很多人第一次碰到装饰器,反应基本是两种:要么绕着走,能不用就不用;要么硬着头皮啃,结果被闭包、作用域、*args绕得头晕眼花。
但说真的,装饰器这东西一旦想通了,你会发现它其实特别朴素------它就是用一个函数去包装另一个函数,仅此而已。没有魔法,没有黑箱,全部逻辑都摆在明面上。今天我们就把这层糖衣剥开,看看里面到底是什么。
装饰器的本质:函数是一等公民
要理解装饰器,得先接受Python里一个基本设定------函数和整数、字符串一样,都是普通的对象。这意味着函数可以被赋值给变量,可以作为参数传给别的函数,也可以作为返回值从函数里被扔出来。
python
def greet():
return "Hello"
say_hi = greet # 函数被当作值赋给了变量
print(say_hi()) # 输出 Hello
这个特性叫函数是一等公民 (first-class citizen)。正是因为有这个前提,才诞生了一个概念叫高阶函数(higher-order function)------接收函数作为参数,或者返回函数的函数。
装饰器,说白了就是一个高阶函数。它接收一个函数func,返回一个新的函数,这个新函数在内部某个时机调用了func,同时在调用前后加了点自己的私货。
用数学的方式表达会更清晰。假设原函数是f,装饰器是D,那么装饰后的行为等价于
g=D(f)
调用 g(x)时,实际执行的是 D内部包裹的逻辑, f(x)只是其中一环。
拆解语法糖:@到底做了什么
先看一个最简单的装饰器。
python
def my_decorator(func):
def wrapper(*args, **kwargs):
print("函数执行前,先打个招呼")
result = func(*args, **kwargs)
print("函数执行完了,再道个别")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
print(add(3, 5))
@my_decorator这一行,其实就是下面这句话的语法糖
python
add = my_decorator(add)
拆开来看这个过程分三步走
- Python先执行
my_decorator(add),把原始的add函数传进去; my_decorator内部定义了一个新函数wrapper,这个wrapper记住了func(也就是原始的add),这就是闭包(closure)在起作用;my_decorator把wrapper这个新函数返回,重新赋值给add这个名字。
所以从此以后,你调用的add其实早已不是原来那个add,而是被套了一层壳的wrapper。壳子里该干嘛干嘛,原函数照样会被执行,只是前后多了点"仪式感"。
闭包 是这里的关键机制------wrapper函数虽然在my_decorator执行完毕后才被真正调用,但它依然能访问到func这个外部变量。这种函数"记住"自己创建时所处环境的能力,就是闭包,也是装饰器能生效的根本原因。
用一张流程图来表示整个执行链条会直观很多。

进阶写法:让装饰器更实用
保留原函数的身份信息
用了装饰器之后,有个小副作用------原函数的__name__、__doc__这些元信息会被wrapper覆盖掉。调试的时候你会发现add.__name__变成了wrapper,这挺让人困惑的。
解决办法是用标准库里的functools.wraps
python
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
加上这一行之后,wrapper会"冒充"原函数的元信息,外界看起来就跟没被装饰过一样,只是行为被悄悄增强了。这个习惯建议养成,几乎是写装饰器的标配。
带参数的装饰器
有时候我们希望装饰器本身也能接受参数,比如指定重试次数、设置缓存时长。这时候需要再包一层。
python
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def say_hello():
print("Hello!")
say_hello() # 会连续打印三次 Hello!
这里其实是三层嵌套函数------最外层repeat负责接收参数,中间层decorator才是真正意义上的装饰器,最内层wrapper负责实际执行。看着绕,但拆开看每一层都很简单,无非是函数生产函数的接力赛。
三大经典应用场景
装饰器真正的价值,不在于炫技,而在于它能把横切关注点(cross-cutting concern)从业务逻辑里剥离出来。所谓横切关注点,指的是那些跟核心业务无关、但又几乎每个函数都要用到的通用逻辑,比如日志、权限、缓存。如果把这些逻辑硬塞进每个函数里,代码会变得又臭又长,装饰器正好能解决这个痛点。
日志记录
想知道函数被调用了多少次、耗时多久、传了什么参数,最土的办法是在每个函数里手动加print或者logging。但用装饰器,只需要写一次,到处复用。
python
import time
from functools import wraps
def log_execution(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
print(f"调用函数 {func.__name__},参数为 {args}, {kwargs}")
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"函数 {func.__name__} 执行完毕,耗时 {elapsed:.4f} 秒")
return result
return wrapper
@log_execution
def slow_function(n):
time.sleep(n)
return "done"
slow_function(1)
这样一来,任何函数只要贴上@log_execution,就自动拥有了日志能力,业务代码里连一行logging都不用写,干净得很。
权限校验
在Web开发里,权限校验是个特别典型的场景。假设某个接口只允许管理员访问,传统写法是在函数体第一行加一堆if判断,装饰器可以把这个判断逻辑抽出去。
python
from functools import wraps
def require_admin(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_admin"):
raise PermissionError("你没有权限执行此操作")
return func(user, *args, **kwargs)
return wrapper
@require_admin
def delete_user(user, target_id):
print(f"用户 {target_id} 已被删除")
admin_user = {"name": "Alice", "is_admin": True}
delete_user(admin_user, 42)
Flask、Django这类Web框架里的@login_required就是这个思路的工业级实现------业务函数只管做自己该做的事,权限这道关卡交给装饰器把守。
缓存
如果一个函数计算量很大,但输入相同时输出总是一样,那完全没必要每次都重新算一遍。缓存装饰器的思路是------第一次调用时把结果存起来,下次遇到相同参数直接返回存好的结果,省时省力。
最简单的手写版本
python
from functools import wraps
def simple_cache(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
print("命中缓存,直接返回")
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@simple_cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(30))
不过实际开发里没必要自己造轮子,Python标准库自带了functools.lru_cache,效果一样,还更成熟。
python
from functools import lru_cache
@lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
lru_cache背后用的是最近最少使用 (Least Recently Used)淘汰策略,当缓存条目超过maxsize时,会自动清理最久没被访问的记录,避免内存无限膨胀。对于递归计算这种场景,效果立竿见影------原本指数级的时间复杂度O(2^n),加了缓存之后直接降到线性的O(n)。
三种场景的对比一览
| 应用场景 | 解决的问题 | 常用工具/写法 | 典型使用位置 |
|---|---|---|---|
| 日志记录 | 追踪函数调用情况和性能 | 自定义装饰器 + logging模块 |
关键业务函数、调试阶段 |
| 权限校验 | 拦截未授权访问 | 自定义装饰器 + 用户状态判断 | Web接口、后台管理功能 |
| 缓存 | 避免重复计算,提升性能 | functools.lru_cache |
递归函数、纯函数、耗时查询 |
三者虽然用途不同,但骨架完全一致------都是在不改动原函数代码的前提下,给它套上一层新能力。这正是装饰器最迷人的地方,它把改动做到了"看不见"的层面。
写在最后
装饰器这套机制说到底,靠的是Python对函数一等公民地位的尊重,再加上闭包这个不起眼却极其强大的语言特性。理解了函数可以被传递、被包裹、被替换,装饰器的谜团基本就解开了大半。
真正让它在工程里发光的,是它对关注点分离 这个原则的践行。业务逻辑归业务逻辑,日志、权限、缓存这些通用能力,统统交给装饰器打理。代码因此变得清爽,复用性也蹭蹭往上涨。下次再看到@符号,希望你不再觉得它神秘,而是能一眼看穿------哦,这不过是个函数,包了另一个函数而已。
参考资料
- Python官方文档,functools --- Higher-order functions and operations on callable objects ,重点参见
functools.wraps部分。 - Flask官方文档,Login Required Decorator示例,展示了权限装饰器在Web框架中的典型实现。
- Python官方文档,functools.lru_cache ,说明LRU缓存淘汰策略及
maxsize参数的作用机制。