📘 Day 27 实战作业:函数魔法 ------ 装饰器 (Decorators)
1. 作业综述
核心目标 :
掌握装饰器(Decorator)的写法与应用。装饰器本质上是一个**"包装器"**,它允许你在不修改原函数代码的情况下,给函数动态地"增加功能"(如计时、日志、权限检查)。
涉及知识点:
- 闭包 (Closure):函数内部定义函数。
- 语法糖 (Syntactic Sugar) :
@decorator_name的写法。 - 通用包装 :使用
*args和**kwargs传递任意参数。
场景类比 :
如果函数是"手机",装饰器就是"手机壳"。
- 手机壳可以防摔(增加鲁棒性)。
- 手机壳可以带支架(增加功能)。
- 最重要的是:装手机壳不需要拆开手机(不修改原代码)。
步骤 1:手动打造第一个装饰器
场景描述 :
我们在调试代码时,经常需要在函数执行前后打印 "开始执行..." 和 "执行结束"。
如果每个函数都手写这两行,代码会很乱。我们写一个 @simple_logger 装饰器来自动完成这件事。
任务:
- 编写一个装饰器
simple_logger。 - 它需要在目标函数执行前打印 ">>> [Start]",执行后打印 "<<< [End]"。
- 用它装饰一个简单的加法函数
add(a, b)并运行。
python
# 1. 定义装饰器函数
def simple_logger(func):
"""
一个简单的日志装饰器
func: 被装饰的目标函数
"""
# 定义内部包装函数 (Wrapper)
# *args, **kwargs 保证了无论 func 有多少参数都能传进去
def wrapper(*args, **kwargs):
print(f">>> [Start] 正在调用函数: {func.__name__}")
# --- 真正执行原函数的地方 ---
result = func(*args, **kwargs)
# -------------------------
print(f"<<< [End] 函数 {func.__name__} 执行完毕")
return result
# 返回包装好的新函数
return wrapper
# 2. 使用装饰器 (语法糖 @)
@simple_logger
def add(a, b):
print(f" 正在计算 {a} + {b} ...")
return a + b
# 3. 测试
# 当你调用 add 时,实际上是在调用 wrapper
res = add(10, 20)
print(f"计算结果: {res}")
>>> [Start] 正在调用函数: add
正在计算 10 + 20 ...
<<< [End] 函数 add 执行完毕
计算结果: 30
步骤 2:实战应用 ------ 性能计时器
场景描述 :
在深度学习中,我们经常需要知道"数据加载花了多久"或者"模型训练一轮花了多久"。
重复写 start = time.time() 和 end = time.time() 很麻烦。
让我们造一个 @timer 装饰器,哪里需要计就在哪里贴一个。
任务:
- 编写
@timer装饰器,计算函数执行耗时。 - 保留原函数的文档字符串 (
functools.wraps),这是一个好习惯。 - 模拟一个耗时任务(使用
time.sleep)并测试。
python
import time
from functools import wraps
def timer(func):
@wraps(func) # 好习惯:保留原函数的元数据(函数名、注释等)
def wrapper(*args, **kwargs):
start_time = time.time() # 记录开始时间
result = func(*args, **kwargs) # 执行原函数
end_time = time.time() # 记录结束时间
duration = end_time - start_time
print(f"⏱️ 函数 [{func.__name__}] 耗时: {duration:.4f} 秒")
return result
return wrapper
# --- 测试计时器 ---
@timer
def heavy_computation():
"""模拟一个耗时的计算任务"""
print(" 开始搬砖 (sleep 1.5s)...")
time.sleep(1.5)
return "Done"
@timer
def process_data(n):
"""模拟处理 n 条数据"""
print(f" 开始处理 {n} 条数据 (sleep 0.5s)...")
time.sleep(0.5)
# 调用
print(f"任务返回值: {heavy_computation()}")
process_data(1000)
# 验证 functools.wraps 的作用
print(f"\n函数说明文档: {heavy_computation.__doc__}")
开始搬砖 (sleep 1.5s)...
⏱️ 函数 [heavy_computation] 耗时: 1.5001 秒
任务返回值: Done
开始处理 1000 条数据 (sleep 0.5s)...
⏱️ 函数 [process_data] 耗时: 0.5028 秒
函数说明文档: 模拟一个耗时的计算任务
步骤 3:高级应用 ------ 自动重试机制 (Robustness)
场景描述 :
在爬虫或下载数据集时,网络可能会抖动。我们希望函数失败时能自动重试 几次,而不是直接报错崩溃。
这是一个非常经典的装饰器应用场景。
任务 :
编写一个带参数的装饰器逻辑(为了简化,我们这里直接硬编码重试 3 次),如果函数抛出异常,就捕获并重试,直到成功或达到最大次数。
python
import random
def retry_3_times(func):
"""
如果函数报错,最多重试 3 次
"""
@wraps(func)
def wrapper(*args, **kwargs):
max_retries = 3
for i in range(max_retries):
try:
print(f"🔄 第 {i+1} 次尝试执行...")
return func(*args, **kwargs) # 尝试执行
except Exception as e:
print(f"❌ 第 {i+1} 次失败: {e}")
if i == max_retries - 1: # 如果是最后一次
print("🚫 达到最大重试次数,放弃治疗。")
raise e # 抛出异常
return wrapper
# --- 模拟不稳定的网络请求 ---
@retry_3_times
def unstable_network_request(url):
# 模拟 70% 的概率失败
if random.random() < 0.7:
raise ConnectionError("连接超时")
print(f"✅ 成功连接到 {url}")
return 200
# 测试 (多运行几次看看效果)
print("--- 开始测试自动重试 ---")
try:
unstable_network_request("www.google.com")
except Exception:
print("程序最终报错停止。")
--- 开始测试自动重试 ---
🔄 第 1 次尝试执行...
❌ 第 1 次失败: 连接超时
🔄 第 2 次尝试执行...
❌ 第 2 次失败: 连接超时
🔄 第 3 次尝试执行...
❌ 第 3 次失败: 连接超时
🚫 达到最大重试次数,放弃治疗。
程序最终报错停止。
🎓 Day 27 总结:代码的"插件化"思维
今天我们掌握了 Python 的 装饰器 。
不要被它的语法吓到,它本质上就是:把一个函数扔进另一个函数里加工一下再拿出来。
装饰器的核心价值:
- 非侵入式 :不想改动
train()函数内部复杂的逻辑,但又想加个计时功能?加个@timer就行。 - 复用性 :写一个
@retry,可以用在下载函数、数据库连接函数、API 请求函数等任何地方。
深度学习伏笔 :
在 PyTorch 中,你会见到 @staticmethod (静态方法)、@property (属性化) 甚至自定义的 Hooks。理解了今天的作业,你再看那些源码时,看到的就不再是"天书",而是一个个精巧的Wrapper。
Next Level: 我们的 Python 基础特训即将收尾。下一阶段,我们将把这些积木搭起来,去解决更复杂的问题!