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) 刻进脑子,所有嵌套都是它的变种。

相关推荐
jason.zeng@150220731 分钟前
(十)分层架构的多文件工程
python·架构·prompt·交互·ai编程·llama
CodeSheep32 分钟前
朋友面试谈薪报价2w,HR非得压到1.9,还反问他:你就差这1000块钱?后来背调时问了他前同事十几个问题,对方最后直接挂了
前端·后端·程序员
haerapi42 分钟前
KingbaseES 全文检索实战之前:先建立中文搜索的可解释基线
开发语言·python·全文检索
徐小黑ACG43 分钟前
Golang 基础02 控制语句
开发语言·后端·golang
Rain的Java大神之路1 小时前
别再瞎装 RabbitMQ 了!从 0 到 1 部署到 Spring Boot 全链路实战,生产级坑全填平
java·后端·面试
专业程序开发源1 小时前
SSM笔记本在线销售系统32649-计算机课程设计、毕业设计
java·spring boot·后端·python·django·php·课程设计
脉动数据行情11 小时前
Python asyncio 异步实现比特币 BTC 实时行情监听 高并发版
开发语言·python·区块链
ClickHouseDB1 小时前
ClickHouse Terraform Provider 正式支持 ClickStack 资源管理
网络·数据库·python
FYKJ_20101 小时前
express绿叶横店短剧推荐与影评分享平台34219-计算机课程设计、毕业设计
java·javascript·vue.js·spring boot·后端·课程设计·express