Menu

Python 装饰器:@ 语法、带参数的装饰器和 functools.wraps

Python 装饰器到底是什么,怎么写你自己的装饰器,以及让它们真正好用的几种写法(参数、叠加、wraps)。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

装饰器是一个包裹函数的函数

这句话听起来很抽象,但原理很简单。装饰器接收一个函数,返回一个函数。它返回的函数通常会调用原来的函数,并在这次调用周围加上一些额外的行为。

最简短的例子:

shout 就是装饰器。它接收一个函数(greet),构建一个新函数(wrapper),新函数调用原函数并把结果转成大写,然后返回这个新函数。重新赋值 greet = shout(greet),就用包裹后的版本替换了原函数。

这种重新赋值的写法太常见了,所以 Python 专门为它设计了一种语法。

@ 是重新赋值的语法糖

在 def 上面一行写 @name,等价于在函数定义之后紧接着写 name = name(...):

@shout 读作“把 shout 装饰器应用到这个函数上”。Python 在 def 之后立即运行 greet = shout(greet):原理和之前一样,只是少打几个字。

以后看到 @name,就在脑子里把它替换成 function = name(function)。这个语法的全部意思就是这样。

处理参数

大多数函数都接收参数。一个可用的装饰器要把参数原样传下去。惯用写法是 *args, **kwargs,这是 Python 接收任意参数的方式,因为包装函数不应该关心被包裹的函数需要什么参数:

*args 捕获所有位置参数。**kwargs 捕获所有关键字参数。包装函数把所有参数原封不动地转发给被包裹的函数,然后做装饰器要做的额外工作,这里是把结果转成大写。

大多数真实的装饰器都是这个样子。

一个更有用的例子:计时

打印一个函数花了多长时间:

在调用之前运行点什么,在调用之后再运行点什么,这正是大多数装饰器最终在做的事。日志、权限检查、重试和输入验证都遵循同样的结构。

保留原函数的身份:functools.wraps

装饰一个函数会替换掉它,这意味着被包裹的函数会丢失原来的 __name__ 和 __doc__ 属性:

greet.__name__ 现在是 "wrapper",文档字符串也不见了。这会破坏 help()、traceback,以及任何检查这个函数的工具。

修复只需要一行:在内部函数上加 @functools.wraps(func),它会把元数据复制过来。

一定要给内部函数加上 @wraps(func)。它没有任何代价,还能让你以后免于在调试时碰到意外。

带参数的装饰器

有时候装饰器本身也需要配置,比如“这个函数最多重试 3 次”“以 DEBUG 级别记录日志”。这就意味着要再多嵌套一层:一个外层函数接收参数,并返回一个装饰器。

三层听起来很多。从外往里读:

  1. repeat(times=3) 是一次函数调用。它返回 decorator。
  2. decorator 才是真正的装饰器:它接收一个函数,返回一个包裹后的函数。
  3. wrapper 是在调用时运行的包裹函数。

@retry(times=5)、@cache(maxsize=100) 以及 @app.route("/users") 这样的框架装饰器,用的都是这种结构。一旦你看懂了这种三层结构,这一整类装饰器读起来都一样。

叠加装饰器

你可以给一个函数应用多个装饰器。它们是“从下往上”叠加的:离 def 最近的那个最先执行:

add_exclaim 先包裹,加上 !。然后 shout 再包裹它,把所有内容转成大写。输出是 HI, ROSA!。

顺序很重要。把顺序反过来,你得到的仍是 HI, ROSA!,只是感叹号是在转成大写“之后”才加上的。在这里看起来一模一样,但想象一下一个格式化 JSON 的装饰器:让它在一个记录输入的装饰器之前还是之后运行,结果可能大不相同。

你会见到的内置装饰器

Python 及其标准库自带了几个装饰器,你在实际代码中会遇到它们:

  • @property 把一个方法变成计算出来的属性。
  • @staticmethod 标记一个不使用 self 或 cls 的方法。
  • @classmethod 接收的是类(cls),而不是实例,非常适合用来写替代构造函数。
  • @functools.lru_cache 会缓存结果,用相同参数重复调用时直接命中缓存。

框架装饰器(@app.route、@pytest.fixture、@dataclass)用的也是同一套机制。没什么特别的,只不过是包裹函数的函数。

什么时候该写,什么时候不该写

当你想把同一种“行为”应用到许多函数上时,就写一个装饰器,比如计时、日志、重试、权限检查。关键在于让这种行为留在函数主体之外。

以下情况就别用装饰器了:

  • 这种行为只属于某一个特定的函数。把它写进那个函数里。
  • 你只是在测试时需要它。用测试夹具或参数会更清楚。
  • 你忍不住想叠加四五个装饰器。到了这个地步,控制流就藏在装饰器链里了,读代码的人得一层层拆开,才能看清实际运行的是什么。一个直截了当的辅助函数可能更好读。

装饰器是一把锋利的工具。用得好,它能让代码不重复、意图清晰。用得不好,它会把程序在做什么藏起来。在决定要不要用装饰器时,倾向于选择“一目了然”的写法。

下一篇:类型提示

装饰器是在实际代码中经常遇到类型提示的地方,wrapper 函数常常会给自己的签名加上注解。类型提示是一个很快就能见效的小功能,下一篇就讲它。

常见问题

Python 中的装饰器是什么?

装饰器是一个函数,它接收另一个函数,返回一个新函数,通常是用额外的行为把原函数包裹起来的函数。使用方法是在 def 上面一行写 @decorator_name。@ 语法是 func = decorator_name(func) 的简写。

Python 中的装饰器有什么用?

在不修改函数主体的情况下,给函数加上前后的行为:日志、计时、缓存、身份验证检查、输入验证、重试。各种框架大量使用装饰器:Flask 中的 @app.route(...),pytest 中的 @pytest.fixture,还有内置的 @property 和 @staticmethod。

我可以自己写装饰器吗?

可以。装饰器只是一个接收函数、返回函数的函数。大多数自定义装饰器都会把原来的调用包进一个小的内部函数,在调用之前、之后或前后做点什么。在内部函数上使用 functools.wraps,可以保留原函数的名字和文档字符串。

Coddy 编程语言插图

和 Coddy 一起学编程

开始