装饰器可以说是 Python 最让人“又爱又恨”的语法糖之一。

刚开始我们觉得它只是个语法糖,可一旦深入,就发现它其实可以做很多:权限验证、日志记录、缓存、参数校验、接口拦截、自动重试……几乎所有能想到的“函数增强”都可以用装饰器实现。

但问题也随之而来:

  • 装饰器到底是怎么“包裹函数”的?
  • 为什么装饰器函数要写三层?
  • @wraps 到底有什么用?
  • 装饰器能不能传参数?
  • 多个装饰器嵌套,顺序怎么搞清楚?

这篇文章,让我们不依赖死记硬背,把装饰器的原理和实际用法讲清楚讲透


装饰器是啥?

装饰器本质上是一个“接收函数,返回新函数”的函数。

说白了就是:它接收一个函数,然后生成一个“升级版”的函数,把你想加的功能(比如打印日志)包在原函数前后。

最小例子:

def outer(func):
def inner():
print("before")
func()
print("after")
return inner

@outer
def say_hello():
print("Hello!")

执行 say_hello(),你会看到:

before
Hello!
after

这就完成了“在不修改原函数的前提下,扩展了功能”。

注意:@outer 实际上就是把 say_hello = outer(say_hello)


为什么装饰器要嵌套这么多层?

你可能看到过这种结构:

def decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper

为什么这么绕?因为你需要:

  1. decorator 用来接收原函数;
  2. wrapper 是新生成的函数,包裹逻辑写在这里;
  3. *args, **kwargs 保证你可以装饰任意函数;
  4. 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):                 # 外层函数:接收 retry 的参数
def decorator(func): # 中层函数:接收被装饰的函数
@wraps(func)
def wrapper(*args, **kwargs): # 内层函数:包裹原函数,添加重试逻辑
for i in range(times): # 重试 N 次
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 结束

顺序解释:

  • 你可以理解为 run = A(B(run)),即 B 先包,A 后包;

    • B(run):先执行 B,把 run 包一层,变成 B_wrapper
    • A(...):再用 A 把 B_wrapper 再包一层,变成最终的 A_wrapper
    • 得到的最终 run 是这样的嵌套结构:run = A_wrapper(B_wrapper(run_body))
  • 执行时是“外层先进,然后一层层剥开”。


类装饰器:当你需要保存状态

普通装饰器只能作用一次,如果你想记录这个函数被调用了几次,怎么办?可以用类来封装状态。

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() 两次,你会看到:

第 1 次调用
Hello
第 2 次调用
Hello

类装饰器的核心是实现 __call__,它允许“实例像函数一样被调用”。

下面拆解一下这个类装饰器的工作方式:

构造方法:__init__

当你写下:

@CountCalls
def greet():
print("Hello")

其实就等价于:

greet = CountCalls(greet)

也就是说:

  • Python 会把 greet 函数传给 CountCalls.__init__()func 参数;
  • 然后返回的是 CountCalls 的实例;
  • 此后,greet() 实际上是调用这个实例对象。

调用方法:__call__

Python 的一个语法特性是:如果一个类实现了 __call__() 方法,那么它的实例就可以像函数一样被调用

所以当写下:

greet()

Python实际执行的是:

greet.__call__()

也就是:

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
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__) # 输出 "wrapper"
print(say_hello.__doc__) # 输出 None

这会影响:

  • 日志打印
  • 调试断点
  • 文档生成工具(如 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") # TypeError: wrapper() takes 2 positional arguments but 1 was given

正确做法:

永远使用:

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 已经执行过,跳过。

运行结果:

初始化配置
函数 init_config 已经执行过,跳过。
函数 init_config 已经执行过,跳过。

写一个 @check_admin 装饰器,只有 admin 用户能访问

我们先假设有一个方法可以拿到“当前用户”的信息,比如:

# 模拟当前登录用户(通常来自 session 或 token)
def get_current_user():
return {"username": "tom", "role": "user"} # 非管理员
# return {"username": "admin", "role": "admin"} # 管理员

装饰器代码实现:

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 = {} # key: 参数组合, value: (返回值, 时间戳)

@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 缓存。