第32课:Python|装饰器入门与进阶【无参装饰器、有参装饰器底层实战】

文章目录
📖 开篇导读
在上一节课中,我们学习了迭代器和生成器,它们体现了惰性求值和函数式编程的思想。今天,我们将探索Python中另一个极其强大且优雅的特性——装饰器(Decorator)。
装饰器是一种在不修改原函数代码的情况下,为函数添加额外功能的技术。它本质上是一个函数(或类),接收一个函数作为参数,返回一个新的函数。装饰器广泛应用于日志记录、性能计时、权限校验、缓存、事务处理等横切关注点(Aspect-Oriented Programming)。
💡 工作场景:
- Web框架中,
@app.route('/')将函数注册为路由处理函数。@login_required装饰器检查用户是否登录。@cache装饰器缓存函数结果,提高性能。@timer装饰器测量函数执行时间,用于性能分析。
本课将系统讲解:
- 装饰器的本质(闭包的应用)
- 无参装饰器的实现与使用
- 带参数的装饰器(三层嵌套)
- 多个装饰器叠加的执行顺序
functools.wraps保留函数元信息- 类装饰器
- 实战案例:计时器、日志、重试、权限校验
学完本课,你将能够编写自己的装饰器,写出更简洁、更优雅的代码。
🎯 学习目标
| 目标编号 | 具体掌握内容 | 对应面试/工作价值 |
|---|---|---|
| 1️⃣ | 理解装饰器的本质(闭包,函数作为参数和返回值) | 面试必问“什么是装饰器” |
| 2️⃣ | 掌握无参装饰器的编写和应用 | 常见的日志、计时等场景 |
| 3️⃣ | 掌握带参数的装饰器的实现(三层嵌套) | 灵活的配置化装饰器 |
| 4️⃣ | 理解多个装饰器叠加的执行顺序 | 调试装饰器链 |
| 5️⃣ | 熟练使用functools.wraps保留函数元数据 |
避免调试困难 |
| 6️⃣ | 了解类装饰器的实现方式 | 替代方案的灵活性 |
🔥 面试考点:“写一个计时器装饰器”“装饰器如何接收参数?”“多个装饰器的执行顺序?”“
@wraps的作用?”
📚 知识点理论精讲
一、装饰器的本质:闭包
在学习装饰器之前,我们先复习闭包(第13课)。装饰器本质上是一个返回函数的函数,利用闭包“记住”被装饰的函数。
1.1 最简单的装饰器
def my_decorator(func):
def wrapper():
print("调用前")
func()
print("调用后")
return wrapper
def say_hello():
print("Hello")
say_hello = my_decorator(say_hello)
say_hello()
输出:
调用前
Hello
调用后
1.2 语法糖 @
Python提供了@语法糖,简化装饰器的使用:
@my_decorator
def say_hello():
print("Hello")
等价于say_hello = my_decorator(say_hello)。
1.3 装饰带参数的函数
装饰器内部的wrapper需要能够接收任意参数,通常使用*args, **kwargs。
def logger(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__},参数: args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"返回值: {result}")
return result
return wrapper
@logger
def add(a, b):
return a + b
add(3, 5)
二、有参装饰器(装饰器工厂)
有时装饰器本身需要参数(例如指定日志级别、重试次数)。这需要再增加一层嵌套,总共三层。
def repeat(times):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def greet(name):
print(f"Hello {name}")
greet("张三")
执行流程:
@repeat(3)先调用repeat(3),返回decorator。@decorator装饰greet,等价于greet = decorator(greet)。
三、多个装饰器叠加
多个装饰器作用于同一个函数时,执行顺序是从下往上(靠近函数的先执行)。
def decorator_a(func):
def wrapper(*args, **kwargs):
print("A before")
result = func(*args, **kwargs)
print("A after")
return result
return wrapper
def decorator_b(func):
def wrapper(*args, **kwargs):
print("B before")
result = func(*args, **kwargs)
print("B after")
return result
return wrapper
@decorator_a
@decorator_b
def hello():
print("Hello")
hello()
# 输出顺序:
# A before
# B before
# Hello
# B after
# A after
记忆:最靠近函数的装饰器(@decorator_b)先执行其wrapper的“before”部分,然后进入外层装饰器。
四、保留函数元信息:functools.wraps
装饰器会覆盖被装饰函数的__name__、__doc__等属性,影响调试和文档生成。使用functools.wraps可以复制这些信息。
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""计时器wrapper"""
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__}耗时: {end-start:.4f}s")
return result
return wrapper
@timer
def slow_function():
"""这是一个慢函数"""
import time
time.sleep(1)
print(slow_function.__name__) # 输出 slow_function(而非wrapper)
print(slow_function.__doc__) # 输出文档字符串
始终使用@functools.wraps编写装饰器。
五、类装饰器
除了函数,也可以使用类来实现装饰器。类需要实现__call__方法。
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 say_hello():
print("Hello")
say_hello()
say_hello()
类装饰器适合需要维护状态的场景(如计数器、缓存)。
六、常见装饰器实战
6.1 计时器
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end-start:.6f}s")
return result
return wrapper
6.2 日志装饰器
def log(level="INFO"):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[{level}] 调用 {func.__name__} 参数: {args} {kwargs}")
return func(*args, **kwargs)
return wrapper
return decorator
6.3 重试装饰器
def retry(max_attempts=3, delay=1):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts+1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"尝试 {attempt} 失败: {e}")
if attempt == max_attempts:
raise
time.sleep(delay)
return None
return wrapper
return decorator
6.4 缓存(记忆化)
def cache(func):
_cache = {}
@functools.wraps(func)
def wrapper(*args):
if args in _cache:
print(f"缓存命中: {args}")
return _cache[args]
result = func(*args)
_cache[args] = result
return result
return wrapper
💻 代码案例实操
案例1:无参装饰器——性能计时器
"""
timer_decorator.py
实现一个通用的性能计时装饰器
"""
import time
import functools
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
elapsed = end - start
print(f"{func.__name__} 执行耗时: {elapsed:.6f}秒")
return result
return wrapper
@timer
def slow_sum(n):
total = 0
for i in range(n):
total += i
time.sleep(0.00001) # 模拟耗时
return total
@timer
def fast_sum(n):
return n * (n - 1) // 2
print(slow_sum(1000))
print(fast_sum(1000))
案例2:有参装饰器——带配置的日志
"""
log_decorator.py
带参数的日志装饰器,可指定输出级别和文件
"""
import functools
import sys
def log(level="INFO", output=sys.stdout):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
msg = f"[{level}] 调用 {func.__name__} args={args} kwargs={kwargs}"
output.write(msg + "\n")
result = func(*args, **kwargs)
output.write(f"[{level}] {func.__name__} 返回 {result}\n")
return result
return wrapper
return decorator
@log(level="DEBUG")
def add(a, b):
return a + b
@log(level="ERROR", output=sys.stderr)
def divide(a, b):
return a / b
add(3, 5)
divide(10, 2)
案例3:多个装饰器叠加——权限校验+日志
"""
multi_decorators.py
演示多个装饰器的执行顺序和组合使用
"""
import functools
def require_auth(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 模拟权限检查
print("权限检查通过")
return func(*args, **kwargs)
return wrapper
def audit_log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"审计日志: 调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
@audit_log
@require_auth
def delete_user(user_id):
print(f"删除用户 {user_id}")
delete_user(1001)
# 输出顺序:
# 审计日志: 调用 delete_user
# 权限检查通过
# 删除用户 1001
案例4:@wraps的重要性演示
"""
wraps_importance.py
演示不使用wraps的后果及使用wraps的好处
"""
import functools
def bad_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
def good_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bad_decorator
def my_func():
"""这是原函数文档"""
pass
@good_decorator
def my_func2():
"""这是原函数文档"""
pass
print("bad_decorator 结果:")
print(" __name__:", my_func.__name__) # wrapper
print(" __doc__:", my_func.__doc__) # None
print("good_decorator 结果:")
print(" __name__:", my_func2.__name__) # my_func2
print(" __doc__:", my_func2.__doc__) # 这是原函数文档
案例5:重试装饰器——处理临时失败
"""
retry_decorator.py
实现一个重试装饰器,支持指数退避
"""
import functools
import time
import random
def retry(max_attempts=3, delay=1, backoff=2, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
current_delay = delay
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
print(f"尝试 {attempt}/{max_attempts} 失败: {e}")
if attempt == max_attempts:
raise
time.sleep(current_delay)
current_delay *= backoff
return None
return wrapper
return decorator
@retry(max_attempts=5, delay=0.5, exceptions=(ValueError,))
def unstable_function():
if random.random() < 0.7:
raise ValueError("随机失败")
return "成功"
try:
result = unstable_function()
print(f"最终结果: {result}")
except ValueError as e:
print(f"重试全部失败: {e}")
案例6:缓存装饰器(LRU简易版)
"""
cache_decorator.py
实现一个简单的缓存装饰器(类似functools.lru_cache)
"""
import functools
def cache(maxsize=128):
def decorator(func):
cache_dict = {}
cache_list = [] # 用于记录访问顺序(简化)
@functools.wraps(func)
def wrapper(*args):
# 注意:只处理位置参数,且要求可哈希
key = args
if key in cache_dict:
print(f"缓存命中: {key}")
# 移到最新(简化版省略)
return cache_dict[key]
result = func(*args)
if len(cache_dict) >= maxsize:
# 删除最早的一个(模拟LRU,这里简化删除第一个)
oldest = cache_list.pop(0)
del cache_dict[oldest]
cache_dict[key] = result
cache_list.append(key)
print(f"计算并缓存: {key}")
return result
return wrapper
return decorator
@cache(maxsize=3)
def fib(n):
if n < 2:
return n
return fib(n-1) + fib(n-2)
print(fib(10)) # 会大量缓存命中,加速计算
案例7:类装饰器——函数调用计数器
"""
class_decorator.py
用类装饰器实现调用计数和重置功能
"""
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
print(f"调用 {self.func.__name__} 次数: {self.count}")
return self.func(*args, **kwargs)
def reset(self):
self.count = 0
print(f"计数器已重置")
@CountCalls
def say_hello():
print("Hello")
say_hello()
say_hello()
say_hello.reset()
say_hello()
案例8:综合实战——带参数的权限装饰器
"""
permission_decorator.py
实现基于角色的权限控制装饰器
"""
import functools
# 模拟用户上下文(全局变量,实际应从请求中获取)
current_user = {"name": "张三", "roles": ["user"]}
def require_permission(required_role):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if required_role not in current_user["roles"]:
raise PermissionError(f"用户 {current_user['name']} 没有 {required_role} 权限")
print(f"权限验证通过: {required_role}")
return func(*args, **kwargs)
return wrapper
return decorator
def login_required(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
if not current_user.get("name"):
raise PermissionError("未登录")
return func(*args, **kwargs)
return wrapper
@login_required
@require_permission("admin")
def delete_user(user_id):
print(f"删除用户 {user_id}")
# 测试
try:
delete_user(100)
except PermissionError as e:
print(e) # 无admin权限
# 提升权限
current_user["roles"].append("admin")
delete_user(100) # 成功
⚠️ 易错点避坑总结
| 序号 | 坑点描述 | 后果 | 解决方案 |
|---|---|---|---|
| 1 | 装饰器内部函数忘记return func(*args, **kwargs) |
原函数的返回值丢失,返回None | 在wrapper中return原函数调用结果 |
| 2 | 不带@functools.wraps |
函数元信息(name, doc)丢失,调试困难 | 始终使用@functools.wraps |
| 3 | 有参装饰器忘记三层嵌套 | 装饰器调用时出错 | 记住:有参装饰器 = 外层函数接收参数,返回内层装饰器 |
| 4 | 多个装饰器顺序错误 | 功能执行顺序与预期不符 | 靠近函数的装饰器先执行(before部分) |
| 5 | 在装饰器中修改可变默认参数 | 经典陷阱,可能累积状态 | 避免在装饰器中使用可变默认参数 |
| 6 | 装饰器不处理关键字参数 | 被装饰函数使用关键字参数时出错 | wrapper使用*args, **kwargs |
| 7 | 类装饰器中忘记调用functools.update_wrapper |
丢失元信息 | 在__init__中调用update_wrapper |
| 8 | 装饰器抛出异常时,原函数未执行 | 逻辑错误 | 根据需求决定是否要try-except |
| 9 | 在装饰器内部定义变量与闭包变量重名 | 意外覆盖 | 注意变量命名 |
| 10 | 对类方法使用装饰器时忘记处理self |
参数错位 | wrapper需要(*args, **kwargs),会正常传递self |
📝 课后实战练习题
第1题:实现@timeit装饰器
编写装饰器@timeit,打印函数执行时间(单位毫秒),精确到小数点后3位。要求使用functools.wraps。
第2题:实现@deprecated装饰器
当调用被装饰的函数时,打印警告信息“函数xxx已弃用,将在未来版本移除”。可以传入替代函数的名称。
第3题:实现@retry装饰器(指数退避)
扩展重试装饰器,支持max_attempts、initial_delay、backoff_factor,并且可以指定需要重试的异常类型列表。
第4题:实现@singleton装饰器
使用类装饰器或函数装饰器,确保被装饰的类只能创建一个实例(单例模式)。测试多次创建得到同一对象。
第5题:实现@validate装饰器
装饰器接收参数类型声明,如@validate(int, str),检查函数参数的类型,不符合则抛出TypeError。
第6题:实现@cache装饰器(支持maxsize)
类似functools.lru_cache,缓存最近使用的maxsize个结果。使用有序字典(collections.OrderedDict)实现LRU。
第7题:综合应用——Web路由模拟
实现一个简易的路由装饰器@route(path),将函数注册到路由表中。然后模拟请求路径,调用对应函数。
🧠 知识点思维导图总结
🔜 下节课预告
装饰器是函数式编程的高级应用。下一节课我们将进入并发编程领域,学习多线程编程基础。
第33课:并发编程基础:多线程创建、生命周期与线程安全详解
内容包括:
- 进程与线程的概念
threading模块创建线程- 线程生命周期与守护线程
- 线程同步(Lock、RLock、Condition)
- 线程安全与GIL
- 实战:多线程下载、生产者消费者模式
并发编程是提升程序性能的重要手段,掌握它你就能编写高效的多任务程序。
🌟 学习鼓励:装饰器是Python的语法亮点,它不仅让代码更简洁,也体现了函数式编程的美妙。初学时可能会对多层嵌套感到困惑,但请多动手写,尤其是带参数的装饰器。坚持练习,你会发现装饰器是提升代码复用性和可读性的利器。
🔗《50节课 Python 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
更多推荐
所有评论(0)