Python 装饰器进阶实战:如何写一个同时支持 @decorator 和 @decorator(...) 的通用装饰器?
在 Python 编程中,装饰器是一种非常优雅的能力。它让我们可以在不修改原函数代码的前提下,为函数增加日志、计时、缓存、鉴权、重试、事务、限流等通用功能。
很多初学者第一次接触装饰器时,通常会写出这样的代码:
@timer
def compute():
...
后来进入真实项目,又会看到这样的写法:
@timer(unit="ms")
def compute():
...
于是问题来了:能不能写一个装饰器,让它既支持 @decorator,又支持 @decorator(...)?
答案是可以,而且这是 Python 实战中非常常见、非常值得掌握的技巧。
这篇文章就从基础原理讲起,逐步拆解这种“可带参数、也可不带参数”的装饰器写法,并结合日志、计时、重试、异步函数等案例,帮助你写出真正可复用、可维护、符合 Python 最佳实践的装饰器代码。
一、先理解两种装饰器写法的本质差异
先看最普通的无参数装饰器:
from functools import wraps
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"开始执行:{func.__name__}")
result = func(*args, **kwargs)
print(f"执行结束:{func.__name__}")
return result
return wrapper
@logger
def say_hello(name):
print(f"Hello, {name}")
这段代码等价于:
say_hello = logger(say_hello)
也就是说,logger 直接接收原函数 say_hello,然后返回包装后的新函数。
再看带参数的装饰器:
def logger(level="INFO"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{level}] 开始执行:{func.__name__}")
result = func(*args, **kwargs)
print(f"[{level}] 执行结束:{func.__name__}")
return result
return wrapper
return decorator
@logger(level="DEBUG")
def say_hello(name):
print(f"Hello, {name}")
它等价于:
say_hello = logger(level="DEBUG")(say_hello)
这里有两步:
第一步,logger(level="DEBUG") 先执行,返回真正的装饰器 decorator。
第二步,decorator(say_hello) 再执行,返回包装后的函数。
所以两种写法的核心区别是:
@logger
等价于:函数 = logger(函数)
@logger(...)
等价于:函数 = logger(...)(函数)
这就是问题的关键:同一个 logger 函数,有时候第一个参数是“被装饰的函数”,有时候第一个参数是“配置参数”。我们要写的通用装饰器,就是要能识别这两种情况。
二、最常用模板:_func=None 加关键字参数
在真实项目中,最推荐的写法是使用一个保留参数 _func=None,再把装饰器配置参数设计成关键字参数。
例如,我们写一个既能直接使用,也能配置日志前缀的装饰器:
from functools import wraps
def log_call(_func=None, *, prefix="LOG", enabled=True):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if enabled:
print(f"[{prefix}] 调用函数:{func.__name__}")
result = func(*args, **kwargs)
if enabled:
print(f"[{prefix}] 函数结束:{func.__name__}")
return result
return wrapper
if _func is None:
return decorator
return decorator(_func)
使用方式一:不带括号。
@log_call
def add(a, b):
return a + b
print(add(3, 5))
使用方式二:带括号,但不传参数。
@log_call()
def multiply(a, b):
return a * b
print(multiply(3, 5))
使用方式三:带括号并传配置参数。
@log_call(prefix="DEBUG", enabled=True)
def divide(a, b):
return a / b
print(divide(10, 2))
这三种写法都能正常工作。
这段代码的关键在这里:
if _func is None:
return decorator
return decorator(_func)
当用户写:
@log_call
def add():
...
Python 会执行:
add = log_call(add)
此时 _func 就是原函数,所以直接返回 decorator(_func)。
当用户写:
@log_call(prefix="DEBUG")
def add():
...
Python 会先执行:
log_call(prefix="DEBUG")
此时 _func 没有传入,默认为 None,所以返回内部的 decorator,等待它再去包装原函数。
这个模板在很多生产项目中都非常实用。
三、为什么建议配置参数使用关键字参数?
你可能注意到了,前面的函数定义中有一个 *:
def log_call(_func=None, *, prefix="LOG", enabled=True):
这个 * 的意思是:后面的参数必须通过关键字传入。
也就是说,允许这样写:
@log_call(prefix="DEBUG")
def task():
...
但不建议这样写:
@log_call("DEBUG")
def task():
...
为什么?
因为装饰器需要判断第一个位置参数到底是“函数”,还是“配置值”。如果允许随意传位置参数,就容易出现歧义。
例如:
@decorator
def f():
...
这里 decorator 收到的第一个参数是函数。
但如果写:
@decorator("admin")
def f():
...
这里 decorator 收到的第一个参数是字符串配置。
一旦装饰器参数复杂起来,代码判断就会变得混乱。为了可读性和稳定性,生产环境中更推荐:
@decorator(role="admin")
def f():
...
这也是 Python 最佳实践中非常重要的一点:让 API 明确,减少隐式猜测。
四、实战案例一:同时支持两种写法的计时装饰器
计时装饰器是 Python 教程中最经典的例子。下面我们写一个更实用的版本,它支持:
@timer
也支持:
@timer(unit="ms", label="数据库查询")
代码如下:
import time
from functools import wraps
def timer(_func=None, *, unit="s", label=None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
cost = time.perf_counter() - start
if unit == "ms":
cost_display = cost * 1000
unit_display = "ms"
else:
cost_display = cost
unit_display = "s"
name = label or func.__name__
print(f"[TIMER] {name} 耗时:{cost_display:.4f} {unit_display}")
return result
return wrapper
if _func is None:
return decorator
return decorator(_func)
使用示例:
@timer
def compute_sum(n):
return sum(range(n))
@timer(unit="ms", label="列表排序")
def sort_numbers(numbers):
return sorted(numbers)
compute_sum(1_000_000)
sort_numbers([5, 3, 8, 1, 2])
输出示例:
[TIMER] compute_sum 耗时:0.0183 s
[TIMER] 列表排序 耗时:0.0121 ms
这个例子很适合放在工具库中。因为有些场景只需要快速计时,用 @timer 就够了;有些场景希望自定义单位和名称,就可以使用 @timer(...)。
一个好的 Python 装饰器,不应该强迫使用者记住复杂写法,而应该在简单场景下足够简单,在复杂场景下又足够灵活。
五、实战案例二:支持参数的重试装饰器
在接口调用、数据库操作、消息队列消费等场景中,重试装饰器非常常见。
我们希望它支持默认重试:
@retry
def fetch_data():
...
也支持自定义重试次数和异常类型:
@retry(times=5, exceptions=(TimeoutError,))
def fetch_data():
...
代码如下:
import time
from functools import wraps
def retry(_func=None, *, times=3, delay=0.5, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as exc:
last_error = exc
print(f"[RETRY] {func.__name__} 第 {attempt} 次失败:{exc}")
if attempt < times:
time.sleep(delay)
raise last_error
return wrapper
if _func is None:
return decorator
return decorator(_func)
使用示例:
@retry
def unstable_task():
print("执行不稳定任务")
raise RuntimeError("临时失败")
@retry(times=5, delay=1, exceptions=(ConnectionError, TimeoutError))
def request_api():
print("请求远程接口")
raise TimeoutError("接口超时")
这个装饰器的实用性非常强。它把重试逻辑从业务代码中抽离出来,使业务函数只关注核心逻辑。
原本你可能要这样写:
for i in range(3):
try:
result = request_api()
break
except TimeoutError:
time.sleep(1)
现在只需要:
@retry(times=3, delay=1, exceptions=(TimeoutError,))
def request_api():
...
这就是装饰器在 Python 实战中的魅力:它让重复的横切逻辑变得统一、清晰、可测试。
六、进阶写法:支持 @decorator("xxx") 这种位置参数形式
有时候,我们也希望装饰器可以这样写:
@tag("order")
def create_order():
...
这时 "order" 是配置参数,不是函数。为了兼容 @tag 和 @tag("order"),可以使用哨兵对象来区分“没有传参”和“传入了 None”。
from functools import wraps
_EMPTY = object()
def tag(arg=_EMPTY, *, level="INFO"):
def decorator(func):
label = None if arg is _EMPTY else arg
@wraps(func)
def wrapper(*args, **kwargs):
tag_name = label or func.__name__
print(f"[{level}] [{tag_name}] start")
result = func(*args, **kwargs)
print(f"[{level}] [{tag_name}] end")
return result
return wrapper
if callable(arg):
func = arg
arg = _EMPTY
return decorator(func)
return decorator
使用方式:
@tag
def task_a():
print("任务 A")
@tag("order")
def task_b():
print("任务 B")
@tag("payment", level="DEBUG")
def task_c():
print("任务 C")
不过这种写法需要谨慎。因为 callable(arg) 的判断并不总是绝对安全。
例如,某些配置参数本身也可能是可调用对象:
@decorator(custom_handler)
def f():
...
此时 custom_handler 可能被误判为被装饰函数。
所以在工程实践中,更推荐下面这种清晰写法:
@decorator(handler=custom_handler)
def f():
...
也就是说,位置参数形式更灵活,但关键字参数形式更稳健。
七、异步函数怎么办?
现代 Python 编程中,异步函数越来越常见,例如 FastAPI、异步爬虫、实时数据处理、异步数据库访问等。
如果普通装饰器直接包装异步函数,可能会出现一个问题:调用异步函数时返回的是协程对象,必须使用 await 才会真正执行。
我们可以写一个同时支持同步和异步函数的计时装饰器:
import inspect
import time
from functools import wraps
def smart_timer(_func=None, *, unit="s"):
def decorator(func):
if inspect.iscoroutinefunction(func):
@wraps(func)
async def async_wrapper(*args, **kwargs):
start = time.perf_counter()
result = await func(*args, **kwargs)
cost = time.perf_counter() - start
if unit == "ms":
cost *= 1000
print(f"[ASYNC TIMER] {func.__name__} 耗时:{cost:.4f} ms")
else:
print(f"[ASYNC TIMER] {func.__name__} 耗时:{cost:.4f} s")
return result
return async_wrapper
@wraps(func)
def sync_wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
cost = time.perf_counter() - start
if unit == "ms":
cost *= 1000
print(f"[SYNC TIMER] {func.__name__} 耗时:{cost:.4f} ms")
else:
print(f"[SYNC TIMER] {func.__name__} 耗时:{cost:.4f} s")
return result
return sync_wrapper
if _func is None:
return decorator
return decorator(_func)
使用示例:
@smart_timer(unit="ms")
def normal_task():
return sum(range(100000))
@smart_timer
async def async_task():
return "async result"
这个例子说明,优秀的装饰器不只是能跑通,还要考虑函数类型、返回值、异常、元信息、调试体验等真实项目问题。
八、不要忘记 functools.wraps
写装饰器时,@wraps(func) 几乎是必需品。
如果不使用它:
def bad_logger(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_logger
def hello():
"""这是 hello 函数"""
print("hello")
print(hello.__name__)
print(hello.__doc__)
输出可能是:
wrapper
None
这说明原函数的名称和文档字符串丢失了。
这会影响很多场景:
调试日志中函数名不准确
自动文档生成失败
测试报告可读性下降
Web 框架路由识别异常
类型检查和 IDE 提示变差
正确写法是:
from functools import wraps
def good_logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
在多个装饰器叠加时,每一层装饰器都应该使用 wraps。这是 Python 装饰器最佳实践中非常基础但非常重要的一条。
九、类型标注:让装饰器对 IDE 更友好
对于资深开发者来说,装饰器还有一个常见痛点:包装后函数类型丢失,IDE 不知道参数和返回值是什么。
在 Python 3.10+ 中,可以使用 ParamSpec 和 TypeVar 改善类型提示:
from functools import wraps
from typing import Callable, TypeVar, ParamSpec, Optional
P = ParamSpec("P")
R = TypeVar("R")
def typed_logger(
_func: Optional[Callable[P, R]] = None,
*,
prefix: str = "LOG",
) -> Callable[[Callable[P, R]], Callable[P, R]] | Callable[P, R]:
def decorator(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"[{prefix}] {func.__name__}")
return func(*args, **kwargs)
return wrapper
if _func is None:
return decorator
return decorator(_func)
类型标注看起来复杂一些,但在大型项目中很有价值。它能让编辑器更准确地提示函数参数,也能减少多人协作中的误用。
不过,对于初学者来说,可以先掌握无类型标注版本。等项目变大、团队协作变复杂,再逐步引入类型系统。
十、常见错误与避坑指南
错误一:忘记返回内部装饰器
错误写法:
def decorator_factory():
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
这里外层函数没有 return decorator,所以 @decorator_factory() 会得到 None,程序自然无法运行。
正确写法:
def decorator_factory():
def decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
return decorator
错误二:忘记返回原函数结果
错误写法:
def logger(func):
def wrapper(*args, **kwargs):
func(*args, **kwargs)
return wrapper
如果原函数有返回值,这个装饰器会把返回值吞掉。
正确写法:
def logger(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
错误三:参数判断过度依赖 callable
callable 很方便,但并非所有场景都安全。因为配置参数也可能是函数、类或其他可调用对象。
更稳健的方式是限制配置项必须使用关键字参数:
def decorator(_func=None, *, option=True):
...
这样 API 更清晰,调用方式更统一。
错误四:异步函数没有 await
错误写法:
def async_logger(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
如果 func 是异步函数,这里返回的是协程对象。正确做法是使用:
async def wrapper(*args, **kwargs):
return await func(*args, **kwargs)
十一、一个生产可用的通用模板
如果你只想记住一个模板,可以记住下面这个版本:
from functools import wraps
def my_decorator(_func=None, *, option1=True, option2="default"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 前置逻辑
if option1:
print(f"before: {option2}")
result = func(*args, **kwargs)
# 后置逻辑
if option1:
print(f"after: {option2}")
return result
return wrapper
if _func is None:
return decorator
return decorator(_func)
使用方式:
@my_decorator
def func_a():
pass
@my_decorator()
def func_b():
pass
@my_decorator(option1=False, option2="custom")
def func_c():
pass
这个模板适合日志、计时、开关控制、简单鉴权、结果包装等大量场景。
十二、从装饰器设计看 Python 的工程美感
Python 之所以被很多开发者喜爱,不只是因为语法简洁,也因为它允许我们用很少的代码表达清晰的设计思想。
一个好的装饰器,背后往往体现了三种工程能力:
第一,抽象能力。把日志、重试、计时、鉴权这些横切逻辑从业务函数中抽离出来。
第二,接口设计能力。让使用者既能简单地写 @decorator,也能在需要时写 @decorator(...)。
第三,边界意识。知道什么时候应该用装饰器,什么时候不该滥用装饰器。
装饰器可以让代码优雅,也可能让代码变得隐晦。真正成熟的 Python 开发者,不只是会写技巧,更知道如何让团队中的其他人读懂、用对、维护好这段代码。
十三、总结
回到本文的核心问题:如何写一个既支持 @decorator 又支持 @decorator(...) 的装饰器?
最推荐的模板是:
def decorator(_func=None, *, option=value):
def real_decorator(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return wrapper
if _func is None:
return real_decorator
return real_decorator(_func)
它背后的本质是:
@decorator
等价于 decorator(func)
@decorator(...)
等价于 decorator(...)(func)
所以我们要判断:当前传进来的到底是原函数,还是装饰器配置。
在实际 Python 实战中,请记住这几条最佳实践:
优先使用 _func=None 模式
配置参数尽量使用关键字参数
每一层包装都使用 functools.wraps
不要吞掉原函数返回值
异步函数要使用 async wrapper 和 await
复杂装饰器要写单元测试
不要为了炫技而过度装饰
Python 编程的成长,往往不是从写出更复杂的代码开始,而是从写出更清晰、更稳定、更容易被别人使用的代码开始。
当你能设计出一个既简单又灵活的装饰器时,你理解的就不只是语法,而是 Python 作为一门工程语言真正迷人的地方。
你在项目中有没有写过日志、缓存、重试、鉴权类装饰器?有没有因为 @decorator 和 @decorator(...) 的差异踩过坑?欢迎在评论区分享你的经验,让更多 Python 开发者少走弯路。


3万+

被折叠的 条评论
为什么被折叠?



