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

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

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


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

python 复制代码
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. 为什么要写成 @?不用行不行?

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

python 复制代码
def add(a, b):
    return a + b
add = log(add)   # 和 @log 等价

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

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

python 复制代码
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__ 被改写了

python 复制代码
print(add.__name__)   # 输出 wrapper,不是 add!

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

python 复制代码
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. 类也能当装饰器

python 复制代码
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__ 让实例可像函数一样被调用。想给函数挂"状态"(比如调用次数),类比闭包更直观。


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

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

排错清单(收藏级)

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

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

相关推荐
飞Link19 小时前
什么是 AirSim?一文读懂无人机仿真平台
python·算法·无人机
曲径海盗20 小时前
【Python 学习第 38 天】for 循环
python·学习笔记·编程入门
小满zs20 小时前
Go语言第十七章(反射)
后端·go
小满zs20 小时前
Go语言第十五章(JSON序列化)
后端·go
在世修行20 小时前
智学文枢:变式题生成引擎
python·ai·pyside·题目生成
小满zs20 小时前
Go语言第十六章(自定义类型)
后端·go
金銀銅鐵21 小时前
[Java] 用 GUI 展示 EnumMap 增删键值对时的内部变化
java·后端·ai编程
学长毕设21 小时前
基于SpringBoot的上门洗车服务系统设计与实现
java·spring boot·后端
On The Road :1 天前
Java 多线程详解:从入门到线程安全
java·python·安全
岁月宁静1 天前
如何设计支持 Skills 的 Agent?一套可扩展的四层架构
python·agent