Python 装饰器进阶实战:如何写一个同时支持 `@decorator` 和 `@decorator(...)` 的通用装饰器?

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+ 中,可以使用 ParamSpecTypeVar 改善类型提示:

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 开发者少走弯路。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铭渊老黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值