装饰器如何工作,常用哪些?

考查重点:包装调用与函数契约。装饰器接原函数并返回包装对象,调用前后加日志/计时等;透传返回异常并用wraps保元信息

本题目录

参考答案

核心回答

Python 装饰器是在函数或类定义时接收被装饰对象,并把返回结果重新绑定到原名称的一种机制。最常见的写法是返回一个包装函数,在调用原函数前后加入计时、日志、鉴权或缓存等能力,同时透传参数、返回值和异常。

@deco 作用于函数 f,可以近似理解成定义后执行 f = deco(f)。装饰器不一定只能是函数,也可以是可调用对象;返回结果也不一定仍是普通函数,例如属性或类方法装饰器会改变访问和绑定方式。

一个计时装饰器怎么写

下面是同步函数的参考实现,使用 perf_counter 计量经过时间。*args、**kwargs 透传参数,return 透传结果,finally 让失败调用也执行计时输出;没有捕获并吞掉原业务异常。

from functools import wraps
from time import perf_counter


def timed(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        started = perf_counter()
        try:
            return func(*args, **kwargs)
        finally:
            elapsed = perf_counter() - started
            print(f"{func.__name__}: {elapsed:.6f}s")
    return wrapper


@timed
def total(a: int, b: int, *, scale: int = 1) -> int:
    """按比例计算合计。"""
    return (a + b) * scale


assert total(2, 3, scale=4) == 20
assert total.__name__ == "total"
assert total.__doc__ == "按比例计算合计。"
assert total.__wrapped__(2, 3, scale=4) == 20

@wraps(func) 会复制函数名、文档等元信息,并设置指向被包装函数的 __wrapped__,有利于调试与内省。它不会替你调用原函数、补上漏写的 return,也不会把包装器的真实参数列表改成原函数的列表。需要给静态类型检查器保留参数类型时,可以进一步使用 ParamSpec 等类型标注;复制元信息与类型检查是不同层次。

示例中的输出只用于演示计时。实际记录时还需避免日志失败覆盖业务异常,并明确统计的是单层包装、整个调用还是重试后的总耗时,不能只看一个数字就当作业务性能结论。

装饰发生在什么时候,多个装饰器如何排序

装饰发生在执行函数定义语句时,通常就是导入模块或执行到相应定义时,而不是每次调用业务函数时。包装函数内部的代码才通常在后续调用时执行。带参数的 @deco(option) 则先调用工厂取得装饰器,再用它装饰目标对象。

若从上到下写 @outer、@inner,近似展开为 f = outer(inner(f)):先应用离函数近的 inner,再应用 outer。如果装饰器表达式本身有副作用,例如两个工厂调用,表达式按源码从上到下求值,随后才由内向外应用,不能把这两件事都概括成“从下往上执行”。

对于常见的前后包装,调用时依次进入 outer、进入 inner、执行原函数,再退出 inner、退出 outer。顺序会改变语义,例如鉴权放在缓存外层,可以在命中缓存前校验权限;缓存键仍需包含必要的身份和权限范围。不能只因为两种排列都能运行,就认为效果相同。

异步函数为什么要单独处理

普通同步包装器调用 async def 函数,只会取得协程对象。如果此时就结束计时,统计到的是创建协程的时间,实际等待和执行还没有完成。下面的异步版本在包装器中 await 原函数,覆盖到异步调用完成或退出的时刻。

import asyncio
from functools import wraps
from time import perf_counter


def timed_async(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        started = perf_counter()
        try:
            return await func(*args, **kwargs)
        finally:
            elapsed = perf_counter() - started
            print(f"{func.__name__}: {elapsed:.6f}s")
    return wrapper


@timed_async
async def async_total(a, b, *, scale=1):
    await asyncio.sleep(0)
    return (a + b) * scale


assert asyncio.run(async_total(2, 3, scale=4)) == 20

此处计量的是经过时间,会包含等待,不能当成纯 CPU 时间。异步异常和取消仍向上传播,finally 执行清理或记录。wraps 本身不会把同步包装器变成协程函数;生成器、异步生成器的逐次迭代也有自己的生命周期,不能直接套这两个例子就声称覆盖了全部执行时间。

常用哪些装饰器

  • @property:把方法作为属性访问的接口,可配合 setter 等控制写入与校验。
  • @classmethod:方法接收调用时相应的类作为隐式第一个参数,常用于替代构造入口。
  • @staticmethod:不自动绑定实例或类,适合放在类命名空间内的独立逻辑。
  • @functools.wraps:编写包装器时保留被包装对象的元信息。
  • @functools.lru_cache、@functools.cache:缓存可复用的计算结果。需要可哈希参数,并考虑失效和内存;不应把有副作用操作或普通协程对象直接当作可重复缓存的结果。
  • 自定义的计时、日志、参数校验和鉴权装饰器,以及框架提供的路由、注册类装饰器:具体返回类型、调用方式和顺序,要看各自契约。

常见追问

闭包和装饰器有什么关系?

上面的包装函数会保存对 func 的引用,带参数的版本还会保存配置,这通常通过闭包完成。闭包是实现手段,装饰器是定义时应用可调用对象并重新绑定的机制,二者并不等同。

忘记返回包装函数或业务结果会怎样?

外层忘记返回包装函数,原名称通常会被绑定成 None;包装函数忘记返回原调用结果,调用者就会拿到 None。使用 wraps 也不会修复这两类错误。

是否任何重试、缓存都适合做成装饰器?

只有当副作用、重试条件、缓存键和失效规则清晰时才合适。一个通用包装不应悄悄把业务调用变成多次扣款,或把不同用户的结果混用。复杂流程需要显式表达状态,不能全部藏进多层装饰。

对应教程

《17-Tools 工具调用》 的“使用 @tool 装饰器”展示了将函数转为框架工具的应用,帮助理解装饰结果未必仍按原函数方式使用;Python 的装饰时机、元信息和同步/异步包装机制可结合本文与语言规范学习。

参考资料

  • 国内:廖雪峰:装饰器:借鉴普通、带参数包装与函数重新绑定和 wraps 示例,解释正文中装饰器工厂、定义时装饰与调用时执行包装的区别。
  • 国内:骆昊:函数高级应用:参考计时逻辑提取为装饰器的过程,说明正文示例中位置参数、关键字参数、返回值和异常怎样透传。
  • 国内:dongwm:Python 3.10 新增的类型系统特性:借鉴 PEP 612 的装饰器例子,解释正文中通用参数包装的静态类型信息损失及 ParamSpec 的作用,区分类型标注与元信息保留。
  • 国外:Python 3.14:functools:update_wrapper、wraps 与缓存装饰器的契约,明确 __wrapped__ 和元信息的用途,也说明哪些结果不适合普通函数缓存。
  • 国外:Python 3.14:Function definitions 与 Coroutines:依据函数定义、装饰器表达式求值、嵌套应用与协程函数语义,解释正文中装饰时机、堆叠顺序和异步包装必须 await 的原因。

相关面经

7 篇

字节跳动

后端 Agent
一面
查看原面经 →
介绍一下 Python 里的装饰器

公司未注明

测试开发岗
一面、二面、三面
查看原面经 →
Python装饰器的实现原理,请手写一个带参数的装饰器。

卓望

全栈开发岗
校招
查看原面经 →
Python中的装饰器了解吗?

恒生电子

AI 应用开发工程师
校招秋招一面
查看原面经 →
谈谈你对python装饰器的理解,它的核心作用是什么?举一个你在实际项目中使用装饰器的场景,说明它解决了什么问题?

百度

Agent Harness 研发工程师
秋招一面
查看原面经 →
说说python的装饰器,生成器,上下文管理的应用场景?
展开剩余 2 篇面经

网易

测试开发工程师
校招秋招一面
查看原面经 →
装饰器,常用的装饰器

快手

面经合集
查看原面经 →
介绍一下Python中的装饰器及其应用场景

收录题集

2 个来源,共4 份题集

编程与数据结构算法

阿东玩 AI · AgentGuide · 2 条原题

查看题集 →

系统设计与后端基础

阿东玩 AI · AgentGuide · 1 条原题

查看题集 →

恒生电子高频面试题

Rocky Ding · AIGC-Interview-Book · 1 条原题

查看题集 →

Python进阶知识高频考点

Rocky Ding · AIGC-Interview-Book · 1 条原题

查看题集 →

相关题目

4 题