装饰器可以说是 Python 最让人“又爱又恨”的语法糖之一。
刚开始我们觉得它只是个语法糖,可一旦深入,就发现它其实可以做很多:权限验证、日志记录、缓存、参数校验、接口拦截、自动重试……几乎所有能想到的“函数增强”都可以用装饰器实现。
但问题也随之而来:
- 装饰器到底是怎么“包裹函数”的?
- 为什么装饰器函数要写三层?
@wraps 到底有什么用?
- 装饰器能不能传参数?
- 多个装饰器嵌套,顺序怎么搞清楚?
这篇文章,让我们不依赖死记硬背,把装饰器的原理和实际用法讲清楚讲透。
装饰器是啥?
装饰器本质上是一个“接收函数,返回新函数”的函数。
说白了就是:它接收一个函数,然后生成一个“升级版”的函数,把你想加的功能(比如打印日志)包在原函数前后。
最小例子:
def outer(func): def inner(): print("before") func() print("after") return inner
@outer def say_hello(): print("Hello!")
|
执行 say_hello(),你会看到:
这就完成了“在不修改原函数的前提下,扩展了功能”。
注意:@outer 实际上就是把 say_hello = outer(say_hello)。
为什么装饰器要嵌套这么多层?
你可能看到过这种结构:
def decorator(func): def wrapper(*args, **kwargs): result = func(*args, **kwargs) return result return wrapper
|
为什么这么绕?因为你需要:
decorator 用来接收原函数;
wrapper 是新生成的函数,包裹逻辑写在这里;
*args, **kwargs 保证你可以装饰任意函数;
return wrapper 是把包装好的新函数交出去。
换句话说,装饰器的“嵌套”是为了兼容性 + 可扩展性。
加个日志:第一个实战装饰器
我们从最经典的“打印日志”开始:
from functools import wraps
def log(func): @wraps(func) def wrapper(*args, **kwargs): print(f"调用函数 {func.__name__}") result = func(*args, **kwargs) print("调用完成") return result return wrapper
@log def say_hello(name): print(f"Hello, {name}")
|
调用 say_hello("Tom") 输出:
调用函数 say_hello Hello, Tom 调用完成
|
注意两点:
@wraps(func) 这一行不是装饰器的核心逻辑,但是装饰器开发的必备礼仪。
- 它保留原函数的函数名、注释、签名信息;
- 不然
say_hello.__name__ 会变成 wrapper,会对调试和文档工具产生困扰。
*args, **kwargs 是必须的,这是为了适配任意参数的函数。
装饰器能传参数吗?
很多人会误以为装饰器不能传参。其实可以,而且还很常用,比如:
@retry(times=3) def unstable_func(): ...
|
实现思路是:再套一层函数,把参数先收下,再生成装饰器。
def retry(times): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for i in range(times): try: return func(*args, **kwargs) except Exception as e: print(f"第{i+1}次重试失败: {e}") print(f"所有 {times} 次重试都失败了") return wrapper return decorator
|
这个装饰器实现了一个“异常捕获 + 自动重试”的机制,结构上是三层嵌套(参数函数 → 装饰器函数 → wrapper 函数),核心逻辑是在 wrapper 中对原函数执行进行 try/except 控制,并根据传入参数 times 来决定重试次数。
调用流程:
@retry(3) → 先执行 retry(3) 返回 decorator
- 然后
decorator(func) 才包住你要增强的函数
看起来复杂,但逻辑很顺:你先把“设定参数”的工作做掉,再交出一个真正的装饰器。
多个装饰器嵌套,顺序到底谁先谁后?
先上代码:
def A(func): def wrapper(*args, **kwargs): print("A: before") result = func(*args, **kwargs) print("A: after") return result return wrapper
def B(func): def wrapper(*args, **kwargs): print("B: before") result = func(*args, **kwargs) print("B: after") return result return wrapper
@A @B def run(): print("run body")
run()
|
输出:
A: before <-- 最外层的 wrapper B: before <-- 内层的 wrapper run body <-- 最原始的函数体 B: after <-- 内层的 wrapper 结束 A: after <-- 最外层的 wrapper 结束
|
顺序解释:
类装饰器:当你需要保存状态
普通装饰器只能作用一次,如果你想记录这个函数被调用了几次,怎么办?可以用类来封装状态。
class CountCalls: def __init__(self, func): self.func = func self.count = 0
def __call__(self, *args, **kwargs): self.count += 1 print(f"第 {self.count} 次调用") return self.func(*args, **kwargs)
@CountCalls def greet(): print("Hello")
|
调用 greet() 两次,你会看到:
类装饰器的核心是实现 __call__,它允许“实例像函数一样被调用”。
下面拆解一下这个类装饰器的工作方式:
构造方法:__init__
当你写下:
@CountCalls def greet(): print("Hello")
|
其实就等价于:
greet = CountCalls(greet)
|
也就是说:
- Python 会把
greet 函数传给 CountCalls.__init__() 的 func 参数;
- 然后返回的是
CountCalls 的实例;
- 此后,
greet() 实际上是调用这个实例对象。
调用方法:__call__
Python 的一个语法特性是:如果一个类实现了 __call__() 方法,那么它的实例就可以像函数一样被调用。
所以当写下:
Python实际执行的是:
也就是:
self.count += 1 print(...) return self.func(*args, **kwargs)
|
类装饰器适用场景
相比函数装饰器,类装饰器的优势在于:
- 可以保存状态(如调用次数、时间、历史数据)
- 可以更容易扩展(比如记录时间戳、日志等)
- 面向对象风格,代码更清晰,适合复杂逻辑
在 Flask 中一定见过它
Flask 的路由就是一个装饰器:
@app.route('/hello') def hello(): return "Hi"
|
你也可以自己写一个装饰器,比如对接口做 token 校验:
def require_token(func): @wraps(func) def wrapper(*args, **kwargs): token = request.headers.get("Authorization") if token != "secret": return jsonify({"error": "Unauthorized"}), 401 return func(*args, **kwargs) return wrapper
@app.route("/secure") @require_token def secure_data(): return jsonify({"data": "只有授权才能看到"})
|
装饰器的本质就是在原函数执行前,拦截请求、检查权限、决定放行与否。
它的作用是在执行真正的业务函数前,先检查请求头中的 token 是否合法,如果不合法,就直接返回 401。
@require_token加到 Flask 路由函数上,表示:客户端访问 /secure 路由时,Flask 会先执行 require_token 装饰器中的逻辑,再决定是否调用 secure_data 函数。
可能踩过的坑
1. 忘记 return 原函数结果
def wrong(func): def wrapper(*args, **kwargs): func(*args, **kwargs) return wrapper
|
如果原函数有返回值,直接被吞了,调用者拿不到结果。
2. 不加 @wraps,函数名变了
错误示例:
def no_wraps(func): def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
|
问题分析:
- Python 函数有元信息,比如
__name__、__doc__、__annotations__
- 如果你不加
@wraps(func),被装饰函数的 __name__ 会变成 "wrapper",不再是原来的函数名。
后果:
@no_wraps def say_hello(): """打印问候语""" print("Hello")
print(say_hello.__name__) print(say_hello.__doc__)
|
这会影响:
- 日志打印
- 调试断点
- 文档生成工具(如 Sphinx)
- Flask/FastAPI 等框架的自动路由注册
正确做法:
from functools import wraps
def with_wraps(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper
|
3. 参数漏写,装饰器不能泛用
错误示例:
def rigid(func): def wrapper(x, y): return func(x, y) return wrapper
|
问题分析:
- 这种写法只适用于两个参数的函数;
- 一旦装饰的是三个参数的函数或无参函数,就报错了。
后果:
@rigid def greet(name): print(f"Hello {name}")
greet("Tom")
|
正确做法:
永远使用:
def wrapper(*args, **kwargs):
|
必须用 *args, **kwargs 保证适配任何函数。
总结
| 错误 |
后果 |
正确写法 |
| 忘记 return |
返回值丢失 |
return func(...) |
| 忘记 @wraps |
名字、注释等信息丢失 |
@wraps(func) 来自 functools 模块 |
| 没写 *args, **kwargs |
只能装饰特定参数函数 |
用 def wrapper(*args, **kwargs) 通用封装 |
总结:装饰器是代码复用的艺术
在实际开发中,你一定遇到过这样的情况:
- 某段逻辑在多个函数中反复出现,比如权限校验、日志打印;
- 每次都 copy paste 一遍,不仅麻烦,还容易出错;
- 你想要一种更优雅的方式,在不改动函数本身的前提下加上这些逻辑。
装饰器,正是为此而生。
它的本质其实很简单:
接收一个函数,返回一个“增强版”的函数。
一旦你理解了这一点,你就可以:
- 把重复代码封装成可复用的“外挂”;
- 像堆积木一样给函数加功能;
- 写出更模块化、更可维护、更 Pythonic 的代码。
装饰器不只是语法糖,更是一种编程思维。掌握它,是走向成熟 Python 开发者的重要一步。
练习题
写一个 @timeit 装饰器,打印函数执行耗时
import time from functools import wraps
def timeit(func): @wraps(func) def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} 执行耗时:{end - start:.4f} 秒") return result return wrapper
@timeit def slow_function(): time.sleep(1.2) print("执行完了")
slow_function()
|
运行结果:
执行完了 slow_function 执行耗时:1.2019 秒
|
写一个 @only_once 装饰器,保证函数只执行一次
应用场景:
代码实现:
from functools import wraps
def only_once(func): has_run = {"called": False}
@wraps(func) def wrapper(*args, **kwargs): if not has_run["called"]: has_run["called"] = True return func(*args, **kwargs) else: print(f"函数 {func.__name__} 已经执行过,跳过。") return wrapper
@only_once def init_config(): print("初始化配置")
init_config() init_config() init_config()
|
运行结果:
初始化配置 函数 init_config 已经执行过,跳过。 函数 init_config 已经执行过,跳过。
|
写一个 @check_admin 装饰器,只有 admin 用户能访问
我们先假设有一个方法可以拿到“当前用户”的信息,比如:
def get_current_user(): return {"username": "tom", "role": "user"}
|
装饰器代码实现:
from functools import wraps
def check_admin(func): @wraps(func) def wrapper(*args, **kwargs): user = get_current_user() if user.get("role") != "admin": print(f"用户 {user['username']} 没有权限访问 {func.__name__}") return "403 Forbidden" return func(*args, **kwargs) return wrapper
@check_admin def delete_user(user_id): print(f"正在删除用户 {user_id}") return "删除成功"
print(delete_user(42))
|
运行结果:
用户 tom 没有权限访问 delete_user 403 Forbidden
|
写一个装饰器支持 @logger(level="info") 的形式
目标写法:
@logger(level="info") def say_hi(name): print(f"Hi, {name}")
|
输出:
[INFO] 调用函数 say_hi Hi, Tom
|
实现代码:
from functools import wraps
def logger(level="info"): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): print(f"[{level.upper()}] 调用函数 {func.__name__}") return func(*args, **kwargs) return wrapper return decorator
|
使用示例:
@logger(level="warning") def delete_data(): print("数据已删除")
@logger(level="debug") def fetch_data(): print("获取数据中...")
delete_data() fetch_data()
|
输出:
[WARNING] 调用函数 delete_data 数据已删除 [DEBUG] 调用函数 fetch_data 获取数据中...
|
可选优化:
使用 logging 模块替代 print:
import logging
logging.basicConfig(level=logging.INFO)
def logger(level="info"): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): log_func = getattr(logging, level, logging.info) log_func(f"调用函数 {func.__name__}") return func(*args, **kwargs) return wrapper return decorator
|
代码解释:
引入并配置 logging 模块
import logging logging.basicConfig(level=logging.INFO)
|
- 引入标准库
logging,用于比 print 更强大、更灵活的日志打印。
basicConfig(level=...) 设置最低日志输出级别(低于该等级的日志不会打印)。
logging.basicConfig(level=logging.INFO)只会显示 INFO 及以上等级的日志,低于 INFO 的不会显示。
- 默认等级是
WARNING,你设置为 INFO 是为了让 logging.info(...) 能打印出来。
Python 日志等级
| 等级名称 |
对应常量 |
用途描述 |
CRITICAL |
50 |
非常严重错误,程序可能崩溃 |
ERROR |
40 |
一般错误,程序可以继续运行 |
WARNING |
30 |
警告(默认等级),可能出问题 |
INFO |
20 |
一般信息,如状态变化、请求等 |
DEBUG |
10 |
详细调试信息 |
NOTSET |
0 |
最低等级,几乎不单独使用 |
写一个 @cache(seconds=5) 装饰器,缓存函数返回结果一段时间
目标用法:
在一定时间内(比如 5 秒)缓存函数的返回值,在缓存有效期内多次调用不再重新执行函数。
@cache(seconds=5) def get_data(): print("函数被执行了") return {"value": 42}
|
第一次调用时执行函数,5 秒内再次调用直接返回缓存结果,不再打印“函数被执行了”。
实现代码:
import time from functools import wraps
def cache(seconds): def decorator(func): cache_data = { "result": None, "timestamp": 0 }
@wraps(func) def wrapper(*args, **kwargs): current_time = time.time() if current_time - cache_data["timestamp"] < seconds: print(f"[缓存命中] 返回上次结果({seconds} 秒内)") return cache_data["result"]
result = func(*args, **kwargs) cache_data["result"] = result cache_data["timestamp"] = current_time print("[缓存更新] 执行了函数") return result
return wrapper return decorator
|
测试代码:
@cache(seconds=5) def get_data(): print("函数真正执行了") return {"value": time.time()}
print(get_data()) time.sleep(2)
print(get_data()) time.sleep(4)
print(get_data())
|
输出示例:
函数真正执行了 [缓存更新] 执行了函数 {'value': 1750830329.2725694} [缓存命中] 返回上次结果(5 秒内) {'value': 1750830329.2725694} 函数真正执行了 [缓存更新] 执行了函数 {'value': 1750830335.275004}
|
说明:
- 缓存使用了字典
cache_data 保持函数结果和时间戳。
- 每次调用时,先检查当前时间与上次调用时间的差值。
- 如果在
seconds 范围内,就直接返回缓存值;否则更新缓存。
支持“不同参数组合分别缓存”的版本
每组参数组合都有独立的缓存副本
缓存对每组参数分别生效,超时后自动失效
代码实现:
import time from functools import wraps
def cache(seconds=5): def decorator(func): _cache_store = {}
@wraps(func) def wrapper(*args, **kwargs): key = (args, tuple(sorted(kwargs.items()))) current_time = time.time()
if key in _cache_store: result, timestamp = _cache_store[key] if current_time - timestamp < seconds: print(f"[缓存命中] 参数: {key}") return result else: print(f"[缓存过期] 参数: {key}")
result = func(*args, **kwargs) _cache_store[key] = (result, current_time) print(f"[缓存更新] 参数: {key}") return result
return wrapper return decorator
|
示例使用:
@cache(seconds=3) def compute(x, y=1): print(f"计算: {x} + {y}") return x + y
print(compute(1, y=2)) print(compute(1, y=2)) print(compute(1, y=3)) time.sleep(4) print(compute(1, y=2))
|
输出结果:
计算: 1 + 2 [缓存更新] 参数: ((1,), (('y', 2),)) 3 [缓存命中] 参数: ((1,), (('y', 2),)) 3 计算: 1 + 3 [缓存更新] 参数: ((1,), (('y', 3),)) 4 [缓存过期] 参数: ((1,), (('y', 2),)) 计算: 1 + 2 [缓存更新] 参数: ((1,), (('y', 2),)) 3
|
关键点解释:
- 参数缓存 key 用
(args, tuple(sorted(kwargs.items()))) 构造,确保:
- 可哈希(作为字典 key)
- 相同参数顺序不同也能识别一致(kwargs 排序)
- 每组参数对应一个
(result, timestamp) 缓存值
- 缓存时效性通过
time.time() 判断
- 相当于简化版带超时的 LRU 缓存。