好的!装饰器(Decorator)是Python的核心特性之一,对OOP设计和函数增强非常关键。让我们通过几个典型问题深入理解:
问题1:基础装饰器执行顺序
def decorator1(func):
print('装饰器1安装')
def wrapper():
print('装饰器1执行前')
func()
print('装饰器1执行后')
return wrapper
def decorator2(func):
print('装饰器2安装')
def wrapper():
print('装饰器2执行前')
func()
print('装饰器2执行后')
return wrapper
@decorator1
@decorator2
def target():
print('目标函数')
print('--- 调用阶段 ---')
target()
请回答:
- 装饰器安装阶段的打印顺序?
- 函数调用时的执行顺序?
- 最终
target指向的对象是什么?
答:
知识点
(1)什么是装饰器
装饰器的定义
装饰器是一个接受函数(或可调用对象)并返回一个新函数或可调用对象的 callable。常用来在不改原函数代码的情况下,给函数“套”上额外功能(比如日志、计时、缓存、权限检查等)。语法糖就是 @decorator 放在函数定义上。
更正式:装饰器是高阶函数(higher-order function)或实现了 __call__ 的类,它接收一个可调用对象并返回一个新的可调用对象。
#装饰器的基本原理
- 一等函数:Python 的函数是对象,可以作为参数传递、作为返回值、赋值等。
- 闭包:装饰器通常用闭包来创建“包装器”函数,包装器在调用时增加额外行为,然后调用原函数。
- 语法糖:写
@dec等同于:foo = dec(foo)—— 装饰发生在函数定义(模块加载)时。
示例
def my_decorator(func):
def wrapper(*args, **kwargs):
print("调用前,做点事")
result = func(*args, **kwargs)
print("调用后,做点事")
return result
return wrapper
@my_decorator
def greet(name):
print("Hello", name)
# 等价于:
# greet = my_decorator(greet)
greet("Alice")
输出:
调用前,做点事
Hello Alice
调用后,做点事
#装饰器的几种形式(常见分类)
-
普通函数装饰器(上面示例)
-
带参数的装饰器(装饰器工厂):外层函数先接收参数,返回真正的装饰器。
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() # 会打印三次 -
类装饰器:通过实现
__call__的类来做装饰器(有状态时常用)。class CountCalls: def __init__(self, func): self.func = func self.count = 0 def __call__(self, *args, **kwargs): self.count += 1 print("call #", self.count) return self.func(*args, **kwargs) @CountCalls def f(): print("run") f(); f() -
装饰方法(instance method):装饰器对方法工作正常,但要注意
self的传递 —— 通常不需要额外处理。 -
装饰异步函数:如果被装饰的是
async def,包装函数也应async def并await原函数。 -
内置/特殊装饰器:
@staticmethod,@classmethod,@property—— 它们实际上是描述器(descriptor)/特殊可调用对象,用法类似但语义不同。
(2)Python代码执行的两个阶段
你提到的装饰器执行流程理解是完全正确的!但关于「安装阶段」和「执行阶段」的分界确实需要更本质的解释。让我们从 Python 解释器的工作机制来彻底理解:
🔧 最底层的原理:Python代码执行的两个阶段
所有.py文件的执行都分为两大阶段:
阶段1️⃣:编译/加载阶段(安装阶段)
- 触发时机:当Python解释器第一次遇到你的代码时(比如import模块或直接运行脚本)
- 核心操作:
- 逐行解析代码
- 遇到函数/类定义时:创建对应的函数对象/类对象
- 遇到装饰器时:立即执行装饰器函数,替换被装饰对象
- 你的例子详解:
实际执行顺序:@decorator1 # 这行代码本身就是可执行语句! @decorator2 # 这行也是! def target(): # 定义函数的动作本身也会产生函数对象 pass- 解释器看到
def target():创建一个函数对象target - 看到
@decorator2:立刻执行target = decorator2(target)
→ 打印"装饰器2安装"
→ 现在target指向decorator2.wrapper - 看到
@decorator1:立刻执行target = decorator1(target)
→ 打印"装饰器1安装"
→ 现在target指向decorator1.wrapper
- 解释器看到
阶段2️⃣:运行时阶段(执行阶段)
- 触发时机:显式调用函数时(如
target()) - 核心操作:
- 执行被装饰器包装后的新函数
- 按装饰器嵌套顺序执行逻辑
🌰 用「做蛋糕」类比理解
# 原料准备阶段(安装阶段)
@加奶油 # ← 现在立刻把蛋糕胚拿去裹奶油
@加草莓 # ← 现在立刻把蛋糕胚放上草莓
def 蛋糕胚(): # ← 先准备基础蛋糕
return "基础蛋糕"
# 吃蛋糕阶段(执行阶段)
吃蛋糕 = 蛋糕胚() # ← 实际吃的是已经处理过的蛋糕
-
安装阶段:厨师在厨房准备蛋糕(解释器加载代码时)
- 先做
基础蛋糕(定义函数) - 立刻加草莓 → 变成「草莓蛋糕」
- 立刻加奶油 → 变成「奶油草莓蛋糕」
- 先做
-
执行阶段:客人吃蛋糕时(调用函数时)
- 吃到的是已经处理好的「奶油草莓蛋糕」
- 感受到的味道顺序:奶油 → 草莓 → 基础蛋糕
🚨 关键总结
-
安装阶段:
- 发生在「代码第一次被解释器读取时」
- 装饰器
@xxx就是立即执行的函数调用 - 会永久修改被装饰的函数对象
-
执行阶段:
- 发生在「显式调用函数时」
- 执行的是被装饰器改造后的新函数
- 装饰器的wrapper逻辑这时才运行
-
为什么叫「安装」:
- 就像给软件安装插件,程序启动时就把装饰器「安装」到函数上
- 不同于运行时临时添加功能
(3)多层装饰器嵌套
多层装饰器嵌套时,装饰器的安装顺序是从近到远,也就是说离被装饰函数最近的装饰器最先被应用,然后逐层向外。这通常被称为“装饰器堆叠”或“装饰器链”。
假设我们有三个装饰器 decorator1、decorator2 和 decorator3,它们依次装饰同一个函数 func,如下所示:
@decorator3
@decorator2
@decorator1
def func():
pass
这个装饰器链的安装顺序如下:
- 最内层的装饰器
decorator1首先被应用到func上。此时,func被decorator1替换。 - 接下来,
decorator2被应用到已经被decorator1装饰过的func上。此时,func被decorator2替换。 - 最后,
decorator3被应用到已经被decorator1和decorator2装饰过的func上。此时,func被decorator3替换。
最终,当你调用 func() 时,实际上是调用了 decorator3 返回的函数,这个函数内部依次调用 decorator2 返回的函数,然后是 decorator1 返回的函数,最后才是原始的 func。
结果及分析:
(1)执行结果:
--- 安装阶段 ---
装饰器2安装
装饰器1安装
--- 调用阶段 ---
装饰器1执行前
装饰器2执行前
目标函数
装饰器2执行后
装饰器1执行后
— 最终指向 —
target = decorator1(decorator2(target)) # 最终指向decorator1的wrapper
(2)结果分析:
🏗️ 安装阶段(装饰阶段)
🔄 步骤1:遇到@decorator2
@decorator1
@decorator2 # 解释器首先处理这一行
def target(): ...
实际操作:
target = decorator2(target)
此时发生:
- 执行
decorator2(target):- 打印
装饰器2安装 - 将原始
target函数作为func参数传入 - 返回
decorator2的wrapper函数
(此时原始target被保存在decorator2.wrapper的闭包中)
- 打印
✅ 此刻内存中的target:
→ decorator2.wrapper
(内含原始target的引用)
🔄 步骤2:遇到@decorator1
@decorator1 # 处理完内层装饰器后处理这一行
def target(): ... # 注意此时的target已是decorator2的wrapper
实际操作:
target = decorator1(target)
此时发生:
- 执行
decorator1(target):- 此时的
target是decorator2.wrapper - 打印
装饰器1安装 - 返回
decorator1的wrapper函数
(此时decorator2.wrapper被保存在decorator1.wrapper的闭包中)
- 此时的
✅ 最终内存中的target:
→ decorator1.wrapper
→ 包含decorator2.wrapper
→ 包含原始target
🎬 执行阶段(调用阶段)
当调用target()时发生的完整堆栈展开:
1. 执行 decorator1.wrapper():
│ print('装饰器1执行前')
│
├──2. 调用闭包中的 func() → 即 decorator2.wrapper():
│ │ print('装饰器2执行前')
│ │
│ ├──3. 调用闭包中的 func() → 即 原始target():
│ │ print('目标函数')
│ │
│ └── print('装饰器2执行后')
│
└── print('装饰器1执行后')
📜 输出结果与执行路径
# 安装阶段输出(模块加载时立即打印):
装饰器2安装 # decorator2(target)执行
装饰器1安装 # decorator1(装饰后的target)执行
# 调用阶段输出:
--- 调用阶段 ---
装饰器1执行前 # decorator1.wrapper开始执行
装饰器2执行前 # decorator2.wrapper开始执行
目标函数 # 原始target执行
装饰器2执行后 # decorator2.wrapper收尾
装饰器1执行后 # decorator1.wrapper收尾
🌐 内存结构可视化
target
│
├── decorator1.wrapper # 最外层包装器
│ │
│ ├── print('装饰器1执行前/后') # 装饰器1的逻辑
│ │
│ └── func → decorator2.wrapper # 闭包捕获
│ │
│ ├── print('装饰器2执行前/后') # 装饰器2的逻辑
│ │
│ └── func → 原始target # 最内层闭包捕获
│ │
│ └── print('目标函数') # 核心逻辑
🔍 关键原理总结
-
安装顺序:从下往上(
@decorator2先于@decorator1应用)- 如同穿衣服:先内衣(
@decorator2) → 再外套(@decorator1)
- 如同穿衣服:先内衣(
-
执行顺序:从上往下(
@decorator1的逻辑先于@decorator2执行)- 如同拆快递:先拆外包装(
@decorator1) → 再拆内包装(@decorator2)
- 如同拆快递:先拆外包装(
-
闭包链:每个装饰器的
wrapper都牢牢捕获了下一层的函数引用
问题2:带参数的装饰器
需要实现一个装饰器,可以定义最大重试次数:
@retry(max_attempts=3)
def fetch_data():
print("尝试获取数据...")
raise ValueError("网络错误")
当函数抛出异常时自动重试,直到成功或达到最大次数。
问题3:类装饰器 vs 函数装饰器
实现一个能统计函数调用耗时的装饰器,分别用:
- 函数装饰器实现
- 类装饰器实现
283

被折叠的 条评论
为什么被折叠?



