创建时间: 2026-08-25最后更新: 2026-08-25

1. 包装

装饰器(decorator)是一种接收函数、再返回新函数的高阶函数。新函数通常会先执行一些附加逻辑,再调用原函数。

例如,很多函数都需要记录「开始执行」和「执行结束」。如果每个函数都手动写一遍,代码会重复。可以把这段公共逻辑放进装饰器:

log-decorator.py
01
def log_call(function):
02
def wrapper():
03
print("开始执行")
04
result = function()
05
print("执行结束")
06
return result
07
08
return wrapper
09
10
def greet():
11
print("你好,Python")
12
13
greet = log_call(greet)
14
greet()

log_call() 接收原来的 greet,返回一个新的 wrapper。重新赋值后,调用 greet() 实际上是在调用包装函数,原来的问候逻辑仍然由 function() 执行。

装饰器的重点是「在不修改原函数主体的情况下,为它增加行为」。日志、计时、权限检查、缓存和重试都适合抽成这种公共包装逻辑。

2. 语法

Python 提供 @ 语法,让装饰器的使用更直观:

at-syntax.py
01
def log_call(function):
02
def wrapper():
03
print("开始执行")
04
result = function()
05
print("执行结束")
06
return result
07
08
return wrapper
09
10
@log_call
11
def greet():
12
print("你好,Python")
13
14
greet()

函数定义前的 @log_call 等价于下面的重新赋值:

equivalent.py
01
def log_call(function):
02
def wrapper():
03
print("开始执行")
04
result = function()
05
print("执行结束")
06
return result
07
08
return wrapper
09
10
def greet():
11
print("你好,Python")
12
13
greet = log_call(greet)
14
greet()

装饰器在函数定义完成时执行一次,而不是每次调用函数时重新执行。也就是说,模块加载过程中先创建 greet,再调用 log_call(greet) 得到新的函数;以后每次 greet() 都只调用已经生成的包装函数。

这也解释了装饰器的两个调用阶段:装饰阶段接收函数并返回包装函数,运行阶段才由包装函数接收真正的业务参数。

3. 参数

真实函数通常有位置参数、关键字参数和返回值,包装函数应该使用 *args 与 **kwargs 接收它们,并把结果返回给调用方:

universal-wrapper.py
01
from functools import wraps
02
03
def log_call(function):
04
@wraps(function)
05
def wrapper(*args, **kwargs):
06
print(f"开始执行:{function.__name__}")
07
result = function(*args, **kwargs)
08
print(f"执行结束:{function.__name__}")
09
return result
10
11
return wrapper
12
13
@log_call
14
def add(left, right):
15
return left + right
16
17
print(add(2, right=3)) # 5

*args 收集位置参数,**kwargs 收集关键字参数;调用原函数时再次展开它们,才能保持原函数的调用方式。return result 也很重要,否则装饰后的函数会丢失原来的返回值。

@wraps(function) 来自 functools,它会把原函数的名称、文档字符串等信息复制到包装函数上。没有它时,add.__name__ 会变成 wrapper,调试、日志和框架读取函数信息时都不够准确。

包装函数还可以处理异常,但不要无意中吞掉异常。下面的 finally 无论成功还是失败都会执行,原异常仍然会继续抛出:

exception-wrapper.py
01
from functools import wraps
02
03
def log_result(function):
04
@wraps(function)
05
def wrapper(*args, **kwargs):
06
try:
07
return function(*args, **kwargs)
08
finally:
09
print(f"调用完成:{function.__name__}")
10
11
return wrapper
12
13
@log_result
14
def divide(left, right):
15
return left / right
16
17
print(divide(6, 2))

4. 配置

有时装饰器本身也需要参数,例如希望调用函数三次。此时需要多包一层:最外层接收配置,中间层接收函数,最内层才接收业务参数:

repeat-decorator.py
01
from functools import wraps
02
03
def repeat(times):
04
def decorator(function):
05
@wraps(function)
06
def wrapper(*args, **kwargs):
07
result = None
08
for _ in range(times):
09
result = function(*args, **kwargs)
10
11
return result
12
13
return wrapper
14
15
return decorator
16
17
@repeat(3)
18
def greet(name):
19
print(f"你好,{name}")
20
21
greet("小明")

@repeat(3) 并不是把数字直接传给 greet()。它会先执行 repeat(3),得到 decorator;再用 decorator(greet) 得到最终的 wrapper。调用 greet("小明") 时,wrapper 才会循环调用原函数三次。

装饰器配置还可以用于权限检查。把权限名称提前传入后,同一个装饰器工厂就能生成不同规则:

permission-decorator.py
01
from functools import wraps
02
03
def require_role(required_role):
04
def decorator(function):
05
@wraps(function)
06
def wrapper(user, *args, **kwargs):
07
if user["role"] != required_role:
08
raise PermissionError("没有执行权限")
09
10
return function(user, *args, **kwargs)
11
12
return wrapper
13
14
return decorator
15
16
@require_role("admin")
17
def delete_article(user, article_id):
18
return f"已删除文章 {article_id}"
19
20
admin = {"name": "小明", "role": "admin"}
21
print(delete_article(admin, 10))

在 FastAPI 项目中,鉴权、日志和限流也可以采用类似思路。不过框架通常已经提供了依赖注入或中间件机制,应该先使用框架的标准能力,再决定是否自定义装饰器。

5. 组合

一个函数可以同时使用多个装饰器。装饰器从下往上应用,但调用时从上往下进入:

stacked.py
01
from functools import wraps
02
03
def uppercase(function):
04
@wraps(function)
05
def wrapper(*args, **kwargs):
06
return function(*args, **kwargs).upper()
07
08
return wrapper
09
10
def add_prefix(function):
11
@wraps(function)
12
def wrapper(*args, **kwargs):
13
return "结果:" + function(*args, **kwargs)
14
15
return wrapper
16
17
@add_prefix
18
@uppercase
19
def message():
20
return "hello"
21
22
print(message()) # 结果:HELLO

上面的写法等价于:

stacked-equivalent.py
01
from functools import wraps
02
03
def uppercase(function):
04
@wraps(function)
05
def wrapper(*args, **kwargs):
06
return function(*args, **kwargs).upper()
07
08
return wrapper
09
10
def add_prefix(function):
11
@wraps(function)
12
def wrapper(*args, **kwargs):
13
return "结果:" + function(*args, **kwargs)
14
15
return wrapper
16
17
def message():
18
return "hello"
19
20
message = add_prefix(uppercase(message))
21
print(message()) # 结果:HELLO

调用 message() 时,先进入外层的 add_prefix 包装函数;它内部再调用 uppercase 包装函数,最后才执行原始的 message。装饰器较多时,要注意顺序,因为交换顺序可能改变最终结果。

Python 自带和标准库中也有很多装饰器,例如类文章中介绍过的 @property、@classmethod、@staticmethod,以及数据类使用的 @dataclass。它们本质上都在函数或类定义完成时,对定义结果进行包装或改造。

异步函数需要使用异步包装函数,否则包装器无法正确等待原函数:

async-decorator.py
01
from functools import wraps
02
03
def log_async(function):
04
@wraps(function)
05
async def wrapper(*args, **kwargs):
06
print("异步任务开始")
07
result = await function(*args, **kwargs)
08
print("异步任务结束")
09
return result
10
11
return wrapper

这类装饰器适合 FastAPI 的异步接口或异步 LangChain 调用。同步函数和异步函数不能直接使用同一种包装方式,需要保持 async 和 await 的调用模型一致。

6. 取舍

装饰器适合给多个函数增加相同的横切逻辑,但它也会增加调用链的间接层。看到一个被装饰的函数时,读者需要跳转到装饰器才能了解完整行为。

可以按照下面的原则判断是否使用装饰器:

  • 多个函数都需要相同的日志、计时、权限或重试逻辑时,适合使用装饰器;
  • 业务逻辑只出现一次,直接写在函数中通常更容易理解;
  • 装饰器应该保持职责单一,不要在一个包装器里同时完成鉴权、缓存和异常转换;
  • 尽量使用 @wraps,保留原函数的名称和文档信息;
  • 包装函数要正确传递参数、返回值和异常;
  • 为异步函数编写装饰器时,要使用 async def 和 await;
  • FastAPI 等框架已经提供中间件或依赖注入时,优先采用框架的标准扩展点。

装饰器不是为了让代码看起来更高级,而是为了把重复的外围逻辑集中维护。只要增加的抽象让调用关系更难理解,就应该考虑改回普通函数或使用更明确的服务对象。

正在验证登录状态
请稍候,验证完成后将继续显示文章内容