news 2026/10/17 4:59:35

从入门到精通详解Python中的装饰器实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从入门到精通详解Python中的装饰器实战指南

前言


装饰器(decorator)常被讲得很玄,但它的底层机制只有一句话:函数是对象,所以函数可以被当作参数传进另一个函数,也可以被另一个函数返回。装饰器就是把这两件事组合起来的固定套路。


初学者最容易卡在三处:



  • 记不住@到底做了什么。记住等价写法f = deco(f)就够,@只是这个赋值的语法糖。

  • 写不出「带参数的装饰器」。其实只是多包一层,从两层变三层。

  • 不知道装饰器还能装饰类,也不知道装饰器可以写成类。


本文按「本质 → 函数装饰器 → 带参装饰器 → 类装饰器 → 装饰类 → 实战要点」的顺序推进。所有示例按 Python 3.8 就能跑的写法给出;需要更高版本的语法会单独标注。


一、本质:@只是赋值语法糖


# 适用于 Python 3.8+
def deco(func):
print("deco 收到:", func.__name__)
return func # 什么都不做,原样返回


@deco
def hello():
return "hi"


# 上面三行等价于:
# def hello(): return "hi"
# hello = deco(hello)

print(hello())

deco 收到: hello
hi

两个必须记住的结论:



  1. 装饰器在函数定义时执行,不是调用时执行。所以print在导入/定义阶段就打印了。

  2. 装饰器必须返回可调用对象。返回None的话,hello这个名字就变成None了。


二、函数装饰器与functools.wraps


一个真正做事的装饰器要返回一个「包装函数」,在调用原函数前后插入逻辑:


# 适用于 Python 3.8+
import functools


def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("begin", func.__name__)
result = func(*args, **kwargs)
print("end", func.__name__)
return result
return wrapper


@logged
def add(a, b):
"""把两个数相加。"""
return a + b


print(add.__name__) # add
print(add.__doc__) # 把两个数相加。

如果去掉@functools.wraps(func),add.__name__会变成wrapper、add.__doc__会变成None。functools.wraps是functools.update_wrapper的便捷形式,按官方文档,它默认拷贝__module__、__name__、__qualname__、__annotations__、__doc__(Python 3.12 起还默认拷贝__type_params__),并给包装函数挂上__wrapped__指回原函数。


三、带参数的装饰器:从两层到三层


想写@retry(3)这种形式,就要多一层:最外层收「配置参数」,中间层是真正的装饰器,最内层是包装函数。


# 适用于 Python 3.8+
import functools


def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator


@repeat(3)
def say(msg):
print(msg)


say("hi")

hi
hi
hi

判断层数的口诀:返回值用不用再被调用一次。repeat(3)返回的是decorator,decorator(say)返回的是wrapper,真正被调用三次的是wrapper。


四、把装饰器写成类


只要一个类实现了__call__,它的实例就是可调用对象,因此也能当装饰器。用类的好处是状态存在实例属性里,比闭包直观。


# 适用于 Python 3.8+
import functools


class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0

def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)


@CountCalls
def ping():
return "pong"


ping()
ping()
print(ping.count) # 2
print(ping.__name__) # ping

这里用functools.update_wrapper(self, func)而不是functools.wraps,因为wraps是给函数用的装饰器语法,而self是实例。类实例有__dict__,update_wrapper会正常把元信息写进去。


五、装饰类


装饰器也能加在class上方。此时被装饰的是类对象,装饰器通常返回一个加工过的类(或一个函数)。这是单例、注册表等模式的常见实现方式。


# 适用于 Python 3.8+
import functools


def singleton(cls):
"""让一个类只产生一个实例。"""
instances = {}

@functools.wraps(cls, updated=())
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]

return get_instance


@singleton
class Config:
def __init__(self):
self.values = {}


a = Config()
b = Config()
print(a is b) # True

这里的updated=()是故意的:functools.wraps默认会把原对象的__dict__更新到包装函数上,对类来说就是搬类命名空间,没什么意义还容易出错,所以传空元组跳过这一步。


六、叠加顺序与标准库里的现成装饰器


多个装饰器叠加时,从下往上执行,最靠近def的最先被应用:


# 适用于 Python 3.8+
def first(func):
def w(*a, **k):
print("first 前")
r = func(*a, **k)
print("first 后")
return r
return w


def second(func):
def w(*a, **k):
print("second 前")
r = func(*a, **k)
print("second 后")
return r
return w


@first
@second
def work():
print("work")


work()

first 前
second 前
work
second 后
first 后

也就是first(second(work))。叠加时要留意「谁包在外面」,异常处理、计时这类装饰器的内外顺序会直接影响结果。


标准库里已经有一批打磨好的装饰器,优先用它们而不是自己造:




装饰器来源作用



@property内置把方法变成属性访问

@classmethod/@staticmethod内置改变首个隐式参数

@functools.lru_cachefunctools带容量上限的缓存(3.2+)

@functools.cachefunctools无容量上限的缓存(3.9+)

@functools.total_orderingfunctools补齐比较方法

@functools.singledispatchfunctools按第一个参数类型分派

@contextlib.contextmanagercontextlib用生成器写上下文管理器

@abc.abstractmethodabc标记抽象方法



七、实战要点:装饰器加在方法上


把普通函数装饰器加在实例方法上时,self会被*args收进去,通常不用特殊处理:


# 适用于 Python 3.8+
import functools


def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("calling", func.__name__)
return func(*args, **kwargs)
return wrapper


class Cart:
def __init__(self):
self.items = []

@logged
def add(self, name):
self.items.append(name)
return len(self.items)


c = Cart()
print(c.add("apple"))

calling add
1

但如果装饰器里要读方法名或做类型判断,就要意识到args[0]是self。另外@classmethod、@staticmethod与其它装饰器叠加时顺序很重要:@classmethod/@staticmethod通常要放在最外层(最上面),让它最后被应用,才能正确地把底层函数包成描述符。


常见坑点



  1. 装饰器没返回可调用对象


❌def deco(f): print(f)没有return,函数名变成None✅ 每个装饰器都要return包装函数(或原函数、或加工后的类)



  1. 忘加functools.wraps


❌ 所有被装饰函数都叫wrapper,文档字符串也没了 ✅ 包装函数上加@functools.wraps(func)



  1. 带参数的装饰器只写两层


❌@retry(3)而retry只接受一个参数,导入就报错 ✅ 三层结构:配置层 → 装饰器层 → 包装层



  1. 以为装饰器在调用函数时才执行


❌ 在装饰器顶层写初始化逻辑,以为会随每次调用重复 ✅ 顶层代码在定义时只执行一次;要每次执行就写进wrapper



  1. 叠加顺序记反


❌ 以为离def远的先执行 ✅ 自下而上套用,等价于outer(inner(func))



  1. 类装饰器里用functools.wraps包类


❌functools.wraps(cls)把类命名空间塞进函数__dict__✅ 用functools.wraps(cls, updated=())跳过__dict__更新



  1. 类装饰器忘了实现__call__


❌@MyDeco而MyDeco实例不可调用,调用时报TypeError✅ 类要实现__call__才配当装饰器



  1. 把@classmethod放在装饰器链的中间


❌@logged写在@classmethod上面,cls绑定异常 ✅@classmethod/@staticmethod放最外层(最上面)


总结




阶段形式关键点



入门函数装饰器f = deco(f),返回包装函数

进阶加functools.wraps保住元信息与__wrapped__

进阶带参数的装饰器三层结构

精通类装饰器实现__call__,状态存实例属性

精通装饰类装饰class,可返回类或函数

精通叠加自下而上,注意内外顺序



装饰器没有「高级技巧」这一说,全部能力都来自「函数是一等对象」这一条。把f = deco(f)记牢、把functools.wraps变成条件反射、把带参装饰器的三层结构练熟,剩下的就是按需求往包装函数里填逻辑而已。




版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 4:55:36

Qt TCP聊天室:从粘包心跳到状态机的底层实现

简介:本资源是一套基于Qt框架实现的多人在线TCP聊天室完整项目,面向C与Qt初学者及网络编程进阶学习者,解决跨平台实时通信、多客户端协同与本地消息持久化等典型问题。压缩包共130个文件,含5个核心cpp源码、3个ui界面设计、2个pro…

作者头像 李华
网站建设 2026/10/17 4:55:11

CLI-Anything:面向意图的代理式命令行架构

1. 项目概述:CLI-Anything 是什么,它解决的不是“命令行工具”而是“人与机器协作的临界点”CLI-Anything 这个名字乍看像一个泛泛而谈的开源项目代号,但拆开来看——CLI 是 Command Line Interface 的缩写,Anything 则直指“任意…

作者头像 李华
网站建设 2026/10/17 4:54:12

superpowers:为AI编程助手装上“上下文外挂”

做 AI 辅助开发这两年,我试过不少工具,但真正让我觉得像给 Copilot、Codex 这类助手装上“外挂”的,是 superpowers 这个 VS Code 扩展。它不是又一个聊天面板,也不是什么代码生成模型,而是一套把“你怎么跟 AI 描述需…

作者头像 李华
网站建设 2026/10/17 4:54:03

SQLAlchemy ORM实战指南:从核心概念到性能优化与踩坑经验

1. 从裸SQL到ORM:我为什么最终选择SQLAlchemy先说下我自己的经历。早几年做项目时,数据库操作基本都是靠手写SQL,当时觉得这样最可控,性能也好把控。但随着项目越做越复杂,表结构从几张涨到几十张,各种关联…

作者头像 李华
网站建设 2026/10/17 4:53:52

网络嗅探与ARP欺骗实验全记录:从抓包到中间人攻击实战

《网络攻防实践》实验三做完有一阵了,一直想把这次实验的完整过程和踩坑记录整理出来。这门课在高校网络空间安全、信息安全相关专业里基本是必修,实验三通常已经脱离了前两次“熟悉命令、配置环境”的起步阶段,开始真正进入攻防对抗的核心环…

作者头像 李华