Python 装饰器到底是个啥?从 @ 符号到手写一个,5 个例子讲透

76 阅读3分钟

系列:Python 底层设计逻辑|类型:原理拆解型|分发:CSDN / 掘金 / 51CTO / 阿里云 / 知乎 字数:约 1600 字

结论先行:Python 里的装饰器(decorator)本质就是一个"接收函数、返回函数"的高阶函数。@log 写在 def foo() 上面,等价于 foo = log(foo)。它不创造新语法,只是把"函数也能当参数和返回值"这件事用语法糖包了一下。搞懂这一点,你就不会再被嵌套 @ 吓到。


1. 最朴素的理解:装饰器就是一个包装工厂

def log(func):
    def wrapper(*args, **kwargs):
        print(f"调用了 {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

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

add(1, 2)   # 打印:调用了 add,然后返回 3

关键:@log 这一行,解释器执行时就是 add = log(add)。你写的 add 还是原来的函数,log 把它接过去,套一层 wrapper 再还给你。所以之后调 add,实际跑的是 wrapper。

2. 为什么要写成 @?不用行不行?

行。@ 只是语法糖,完全可以写成:

def add(a, b):
    return a + b
add = log(add)   # 和 @log 等价

为什么还要 @:因为它把"增强逻辑"贴在函数定义处,一眼就能看出这个函数被加工过。代码读起来是"声明式"的,比在底部再赋值清爽太多。

3. 带参数的装饰器:其实是三层嵌套

def repeat(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper
    return decorator

@repeat(3)
def hi():
    print("hi")

hi()   # 打印三次 hi

拆解:@repeat(3) 先执行 repeat(3),返回 decorator;再拿 decorator 去包 hi。所以是"函数→返回装饰器→返回 wrapper"三层。看到括号就多一层,这是铁律。

4. 一个真实的坑:__name__ 被改写了

print(add.__name__)   # 输出 wrapper,不是 add!

问题:装饰后函数名、文档字符串全变成 wrapper 的,调试和日志会乱。解法是用 functools.wraps:

from functools import wraps

def log(func):
    @wraps(func)               # 把原函数元信息复制过来
    def wrapper(*args, **kwargs):
        print(f"调用了 {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

记住:只要你手写装饰器,第一件事就是加 @wraps(func),别省。

5. 类也能当装饰器

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0
    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"第 {self.count} 次调用")
        return self.func(*args, **kwargs)

@CountCalls
def hello():
    print("hello")

hello(); hello()   # 第1次 / 第2次

要点:类装饰器靠 __call__ 让实例可像函数一样被调用。想给函数挂"状态"(比如调用次数),类比闭包更直观。


对照表:一眼分清几种写法

写法等价展开适用场景
@logf = log(f)无参数增强(日志/计时/鉴权)
@repeat(3)f = repeat(3)(f)需要配置参数的增强
类 @CountCallsf = CountCalls(f)增强过程要保存状态
不加 @wraps—别这么干,元信息会丢

排错清单(收藏级)

  • @xxx 报错"xxx is not callable":八成是把装饰器当函数用了,少写一层返回。
  • 带参装饰器套错层数:记住"有括号就多一层",@a(b) 是 a(b)(f)。
  • 函数名变成 wrapper:忘了 @wraps,加上即可。
  • 装饰器顺序影响结果:多个 @ 从下往上包,越靠上越外层,顺序不同行为不同。
  • 想装饰类里的方法:@staticmethod/@property 本身也是装饰器,原理一样。

一句话记住:装饰器就是"函数进、函数出"的包装机,@ 只是把这件事写得更好看。先把 foo = deco(foo) 刻进脑子,所有嵌套都是它的变种。