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

简介: 装饰器(decorator)本质是一个「接收函数、返回函数」的高阶函数,@log 写在 def foo() 上等价于 foo = log(foo)。本文从最朴素的包装工厂讲起,用 5 个例子把 @ 语法、functools.wraps、带参数装饰器、类装饰器一次讲透。

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

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


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

写法 等价展开 适用场景
@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) 刻进脑子,所有嵌套都是它的变种。

相关文章
|
4天前
|
缓存 Python
Python 装饰器把函数"搞丢"了?5 个必踩的坑与完整避坑对照表
装饰器的坑不是"不会写",而是写完原函数被悄悄换掉——名字、文档、签名、身份都可能变。functools.wraps 救得回大部分,救不了全部。5 个坑逐个给复现代码和修法,附完整对照表。
|
自然语言处理
使用ChatGPT润色学术论文的9个必备提示词指令,高效实用,值得收藏
本指南涵盖学术写作全流程,从语法校对到逻辑优化,再到格式规范,提供九大角色精细润色指导,助力提升论文质量与学术表达水平。
|
7月前
|
人工智能 Linux API
OpenClaw全自动小红书运营实战:从0到1全流程部署、技能配置与内容自动化发布指南
在内容自动化运营场景中,OpenClaw(Clawdbot)凭借高度可扩展的Skill体系与多任务执行能力,可实现从热点追踪、文案创作、封面生成到笔记发布、互动管理的全流程自动化。本文基于2026年最新环境,完整讲解如何通过阿里云轻量服务器或本地Windows11/macOS/Linux部署OpenClaw,安装并配置小红书运营Skill,完成Cookie登录、内容生成、笔记发布、数据监控,并接入阿里云百炼Coding Plan免费大模型与QMD记忆优化系统,实现低成本、7×24小时无人值守小红书运营。全文无营销词汇,所有命令可直接复制,零基础用户也能快速跑通全流程。
3395 9
|
1天前
|
存储 人工智能 自然语言处理
千问办公官网入口链接在哪?2026年最新QwenWork测评、千问办公怎么样?一文看懂
千问办公(QwenWork)是阿里云推出的AI智能办公平台,提供网页端和阿里云官网双入口。支持PPT/Word/Excel生成、多模态处理、网页全栈搭建、钉钉深度集成等6大核心能力。个人版免费可用,高级版198元/月;企业版198元/人/月。新用户注册即赠2000积分。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
239 0
|
2天前
|
安全 Python 容器
Python 深拷贝和浅拷贝到底差在哪?6 个让你半夜 debug 的案例 + 对照表
浅拷贝只复制第一层,嵌套的可变对象(list/dict/set)仍共享同一块内存;深拷贝递归复制所有层,新旧对象彻底解耦。6 个半夜 debug 的真实案例 + 对照表,把 copy.copy / copy.deepcopy 的坑一次讲清。
|
3天前
|
缓存 Python 容器
Python 里 `is` 和 `==` 到底差在哪?5 个让你怀疑人生的案例 + 对照表
is 比的是对象身份(id 相同),== 比的是值(__eq__ 结果)。本文用 5 个真实案例讲透二者区别,附对照表与最佳实践,并避坑小整数池 / 字符串驻留陷阱。
|
5天前
|
测试技术 数据库 Python
pytest 写了用例却不跑、fixture 不生效、参数化全失效?9 个高频坑对照表
pytest 里 90% 的用例不执行、fixture 报错、参数化失效,都不是框架 bug,而是命名约定和作用域没踩准。9 组「症状→原因→修法」对照表一次讲清。
|
6天前
|
Linux iOS开发 Python
Python 虚拟环境到底怎么选?venv / conda / pipenv / poetry 一张对比表 + 9 个真实踩坑
venv、conda、pipenv、poetry 到底怎么选?本文用一张对比表 + 9 个真实踩坑讲清楚:什么时候 venv 就够了、什么时候必须上 poetry、conda 真正解决的是什么问题,以及虚拟环境里最常见的路径与依赖冲突该怎么排查。
|
7天前
|
数据库 iOS开发 MacOS
Python 模块入口入门指南:if __name__ == '__main__' 到底在防什么?
几乎每个 Python 文件都有 if __name__ == __main__,但很少有人说清它在防什么。本文用可复现的代码讲透 4 类真实事故:多进程递归崩溃、导入即连数据库、-m 与文件路径的差异、pytest 拿不到准备逻辑,并给出什么时候其实可以不写。
|
8天前
|
数据采集 安全 调度
Python 并发编程入门指南:GIL 到底是什么?为什么多线程跑不快
GIL(全局解释器锁)是 CPython 的一把进程级互斥锁,强制单时刻只有一个线程执行字节码。本文用 3 段真实代码实测,讲透 GIL 的本质、它为什么只卡 CPU 密集任务、以及多进程/asyncio 该怎么选。

热门文章

最新文章