Python 装饰器深度指南:从原理到高级实战用法

装饰器(decorator)是 Python 里最有"Python 味"的特性之一,也是新手觉得最"玄"的语法 —— 一个 @ 符号往函数头上一放,函数就莫名其妙多了功能。但其实装饰器一点都不玄,它背后是三个非常朴素的概念的组合。这篇从原理讲起,一步步推到 functools.wraps、带参数的装饰器这些进阶用法,看完你不仅会用,还能自己写。

地基:理解装饰器前必须接受的三件事

理解装饰器前,先接受三个事实:

  1. 在 Python 里,函数是"一等公民" —— 函数可以像普通值一样,
     被赋值给变量、当参数传递、当返回值返回。

  2. 函数里可以定义函数(嵌套函数)。

  3. 内层函数能记住它"出生时"所在的环境(闭包)。

  装饰器 = 这三件事的组合:一个"接收函数、返回新函数"的函数。

这三件事里,最关键的是"函数是一等公民"。在很多语言里,函数就是函数,不能当变量用。但在 Python 里,函数和数字、字符串一样,是可以被赋值、被传递、被返回的"普通值"。装饰器的全部"魔法",不过是把这三件事组合起来 —— 它本质上就是一个"接收一个函数、返回一个新函数"的函数

手写第一个装饰器

不用任何特殊语法,我们先手动实现一个 —— 给函数加上"自动计时"的功能:

手写一个最朴素的装饰器:给函数加"计时"功能

  import time

  def timer(func):                  # 接收一个函数
      def wrapper(*args, **kwargs):  # 定义一个"包装"函数
          start = time.time()
          result = func(*args, **kwargs)   # 调用原函数
          print(f"{func.__name__} 耗时 {time.time() - start:.4f}s")
          return result
      return wrapper                # 返回包装函数

  # 用法一:手动包一下
  def slow_task():
      time.sleep(1)
  slow_task = timer(slow_task)      # 把 slow_task 换成被包装的版本

逐行看 timer 做了什么:它接收一个函数 func;在内部定义了一个 wrapper 函数,wrapper 里先记时间、再调用原来的 func、最后打印耗时;然后把 wrapper 返回出去。

最后一行 slow_task = timer(slow_task) 是精髓:我们把 slow_task 这个名字,重新指向了"被包装后的版本"。之后再调用 slow_task(),实际跑的是 wrapper —— 它会先计时、再调用原始逻辑。原函数没被改一个字,功能却被"增强"了。*args, **kwargs 的作用是让 wrapper 能接收任意参数并原样转交,这样它才能包装任何函数。

@ 语法:它只是语法糖

每次都写 x = timer(x) 有点啰嗦,Python 给了个语法糖 —— 就是那个 @:

@ 语法:只是上面那行的"语法糖"

  @timer
  def slow_task():
      time.sleep(1)

  # 完全等价于:
  def slow_task():
      time.sleep(1)
  slow_task = timer(slow_task)

  所以 @decorator 没有任何魔法,它就是
  "把下面这个函数,传给 decorator,再用返回值替换掉原名字"。

看清楚了:@timer 和手写 slow_task = timer(slow_task)完全等价的。@ 没有任何魔法,它就是"把紧跟在下面的函数,作为参数传给 @ 后面那个东西,再用返回值替换掉原来的名字"。一旦你建立起"@decoratorfunc = decorator(func)"这个等式,装饰器对你就再没有任何神秘感了。

进阶一:用 functools.wraps 保住"身份"

上面的装饰器有个隐藏副作用:被装饰之后,slow_task.__name__ 不再是 'slow_task',而变成了 'wrapper' —— 因为它现在指向的确实是 wrapper 函数。函数的文档字符串、签名等元信息也都丢了。这在调试、日志、以及一些依赖函数元信息的框架里会出问题。

修复办法是 functools.wraps:

两个进阶:functools.wraps 和 带参数的装饰器

  # 问题:被装饰后,slow_task.__name__ 变成了 'wrapper',
  #       文档字符串也丢了。用 functools.wraps 修复:
  from functools import wraps

  def timer(func):
      @wraps(func)                  # ← 把原函数的元信息搬到 wrapper 上
      def wrapper(*args, **kwargs):
          ...
      return wrapper

  # 带参数的装饰器 = 在外面再套一层函数
  def retry(times):                 # 接收"装饰器的参数"
      def decorator(func):          # 这一层才是真正的装饰器
          @wraps(func)
          def wrapper(*args, **kwargs):
              for i in range(times):
                  try:
                      return func(*args, **kwargs)
                  except Exception:
                      if i == times - 1:
                          raise
          return wrapper
      return decorator

  @retry(times=3)                   # 失败自动重试 3 次
  def call_api():
      ...

@wraps(func) 加在 wrapper 上,作用是把原函数 func 的名字、文档等元信息"搬"到 wrapper 上,让被装饰后的函数"对外看起来还是原来那个"。写装饰器时,加上 @wraps 应该成为肌肉记忆。

进阶二:带参数的装饰器

有时我们希望装饰器本身能接收参数,比如 @retry(times=3) —— 重试 3 次。这怎么实现?

诀窍是再套一层函数。回顾一下:@decorator 要求 decorator 是个"接收函数、返回函数"的东西。而 @retry(times=3) 里,retry(times=3)先被执行,它的返回值才是真正的装饰器。所以 retry 的结构是"三层":最外层接收装饰器参数(times),中间层是真正的装饰器(接收 func),最内层是 wrapper(接收调用参数)。上面 $py4 代码里的 retry 就是这个三层结构。

记住这个推导:@retry(times=3) 等价于 call_api = retry(times=3)(call_api) —— 两次调用,所以两层嵌套(再加上 wrapper 共三层)。把这个等式写出来,带参数装饰器的结构就一目了然了。

装饰器到底用在哪

装饰器的核心价值是"把横切关注点(cross-cutting concern)从业务逻辑里抽出来" —— 那些"很多函数都需要、但又跟业务本身无关"的逻辑。典型场景:

  • 日志 / 计时 / 性能监控:像上面的 timer,给一批函数统一加埋点。
  • 缓存:标准库的 functools.lru_cache 就是个装饰器,一行加上,函数自动带缓存。
  • 权限校验:Web 框架里常见的 @login_required,在进入业务逻辑前先检查身份。
  • 重试 / 限流 / 熔断:像上面的 @retry,给不稳定的外部调用加保护。
  • 注册:框架里用 @app.route('/path') 把函数注册成路由处理器。

它们的共同点:这些逻辑如果硬写进每个业务函数里,会重复、会污染业务代码;用装饰器抽出来,业务函数保持干净,关注点彻底分离。

写在最后

装饰器的"祛魅"过程,其实就是认清这几步:

  • Python 里函数是一等公民,可传递、可返回;
  • 装饰器就是一个"接收函数、返回新函数"的函数;
  • @decorator 只是 func = decorator(func)语法糖,没有魔法;
  • 写装饰器记得加 @functools.wraps 保住原函数的元信息;
  • 带参数的装饰器 = 再套一层,因为 @deco(arg) 是先调用、再装饰。

把"@ 就是语法糖"这句话刻进脑子,你再看任何框架里满天飞的装饰器,都能一眼看穿它在做什么。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

C# async/await 深度解析:异步编程背后到底发生了什么

2026-5-14 17:19:07

技术教程

Claude Code 高级使用指南:CLAUDE.md、子代理、MCP 与高效工作流

2026-5-14 17:19:08

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索