← 返回题目列表

Python 装饰器的原理是什么?为什么要用 functools.wraps?

高频 中等 第 14 / 21 题 更新于 2026/07/27
装饰器闭包wraps函数增强

简化版

装饰器本质上是一个接收函数并返回新函数的高阶函数。@decorator 等价于 func = decorator(func)functools.wraps 用来保留原函数的名称、文档、注解等元信息,避免调试、日志和框架识别出问题。

详细版

最简单的装饰器:

def log(func):
    def wrapper(*args, **kwargs):
        print("before")
        result = func(*args, **kwargs)
        print("after")
        return result
    return wrapper

@log
def add(a, b):
    return a + b

等价于:

def add(a, b):
    return a + b

add = log(add)

调用 add(1, 2) 时,实际调用的是 wrapper(1, 2)wrapper 内部再调用原来的 add

推荐写法:

from functools import wraps

def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"call {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

不用 wraps 时,装饰后的函数名会变成 wrapper__doc____annotations__ 等信息也可能丢失。

完整版教学

一、装饰器解决什么问题

装饰器用于在不修改原函数代码的前提下,为函数增加通用能力。

常见场景:

  • 日志记录;
  • 权限校验;
  • 性能计时;
  • 缓存;
  • 事务控制;
  • 重试;
  • 参数校验。

例如业务函数只关心业务:

def get_user(user_id):
    return query_user(user_id)

如果每个函数都手写日志、耗时统计、异常处理,业务代码会被横切逻辑污染。装饰器可以把这些通用逻辑抽出去。

二、函数是一等对象

装饰器能成立,是因为 Python 中函数是一等对象。函数可以:

  • 赋值给变量;
  • 作为参数传给另一个函数;
  • 作为返回值返回;
  • 放进列表或字典。
def hello():
    return "hello"

f = hello
print(f())

装饰器正是利用了“函数可以作为参数和返回值”这个特性。

三、@语法的真实含义

下面代码:

@log
def add(a, b):
    return a + b

等价于:

def add(a, b):
    return a + b

add = log(add)

所以装饰器不是运行时每次调用前才“临时包一下”,而是在函数定义完成后,立即把函数对象传给装饰器,并把返回值重新绑定给原函数名。

这也是为什么装饰器可以改变函数行为:

def always_zero(func):
    def wrapper(*args, **kwargs):
        return 0
    return wrapper

装饰后,函数名已经指向 wrapper,不是原始函数。

四、为什么 wrapper 要接收 *args 和 **kwargs

如果装饰器只写固定参数:

def wrapper():
    return func()

那它只能装饰无参函数。为了让装饰器适配各种函数签名,通常写:

def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

这表示调用者传来的所有位置参数和关键字参数,都原样转交给原函数。

同时要记得返回原函数结果:

result = func(*args, **kwargs)
return result

如果忘了 return,被装饰函数的返回值会变成 None

五、functools.wraps 的作用

不用 wraps

def log(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log
def add(a: int, b: int) -> int:
    """add two numbers"""
    return a + b

print(add.__name__)  # wrapper

这对调试、文档生成、日志、Web 框架路由、测试工具都不友好。

使用 wraps

from functools import wraps

def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

wraps(func) 会把原函数的一些元信息复制到 wrapper 上,并设置 __wrapped__,让工具有机会追溯到原函数。

六、装饰器的副作用和注意点

写装饰器时常见问题:

  • 忘记返回 wrapper
  • wrapper 忘记返回原函数结果;
  • 不使用 wraps 导致元信息丢失;
  • 装饰器吞异常后没有明确处理策略;
  • 装饰异步函数时写成同步 wrapper,导致协程没有被正确 await。

异步函数装饰器要写成:

from functools import wraps

def log_async(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        print("before")
        return await func(*args, **kwargs)
    return wrapper

七、常见误区与追问

记忆钩子:装饰器不是“给函数加注释”,而是把函数名重新绑定到另一个可调用对象;wraps 则是在换壳以后尽量保留原函数身份。

  • 误区:装饰器会在每次函数调用时重新装饰一次。 @log 的应用发生在函数定义完成后,等价于 add = log(add);之后每次调用 add(),调用的是已经返回的 wrapper
  • 追问:为什么 wrapper 通常要写 *args, **kwargs 装饰器往往要适配不同签名的函数,透传位置参数和关键字参数能保持通用性;如果 wrapper 写死参数,就只能装饰特定形态的函数。
  • 误区:装饰器里调用了原函数就一定保留返回值。 如果 wrapper 里写了 func(*args, **kwargs) 却忘记 return,调用方拿到的就是 None,这是业务装饰器里很常见的隐藏 bug。
  • 追问:functools.wraps 到底复制了什么? 它基于 update_wrapper 复制 __name____doc____module____annotations__ 等元信息,并设置 __wrapped__ 方便 inspect 等工具追溯原函数。
  • 误区:用了 wraps 就完全等同于原函数。 wraps 主要保留元信息,不会自动修复 wrapper 的调用语义;异常处理、异步 await、返回值、参数校验仍要由装饰器代码正确实现。
  • 追问:装饰 async 函数时为什么要特别小心? 原函数是协程函数时,wrapper 也应是 async defawait func(...);否则可能返回一个未被 await 的协程对象,导致逻辑没有真正执行。

八、加强记忆

装饰器就是“函数进,函数出”,@log 等价于 func = log(func)wrapper 负责增强逻辑,并用 *args, **kwargs 透传参数;wraps 负责保留原函数身份信息。面试讲清这三层,装饰器原理就稳了。