30天入门Python(基础篇)——第24天:Python中的装饰器详解
📅 学习日期:第24天 | ⏱️ 预计用时:60分钟 | 📊 难度等级:⭐⭐⭐⭐
🎯 学习目标
欢迎大家关注此公众号,后台留言"python书籍"可免费获取【Python办公自动化高清PDF】电子书一本
此外小庄推荐一本适合于新手\小白入手一本 Python基础书籍,欢迎大家订阅,也感谢大家支持,我才有更新的动力
一、什么是装饰器?
1.1 装饰器的概念
装饰器(Decorator)是 Python 中一个非常强大且优雅的特性。从本质上讲,装饰器就是一个函数,它接受一个函数作为参数,返回一个新的函数。装饰器允许我们在不修改原有函数代码的情况下,为其添加额外的功能。
打个比方:装饰器就像给手机套上一个手机壳。手机本身的功能不变,但手机壳给它增加了防摔、美观等额外特性。
1.2 装饰器的设计原则
装饰器遵循 开放-封闭原则(Open-Closed Principle):
二、函数装饰器的基础
2.1 最简单的装饰器
defmy_decorator(func):
"""一个简单的装饰器"""
defwrapper():
print("在函数执行之前")
func()
print("在函数执行之后")
return wrapper
@my_decorator
defsay_hello():
print("Hello!")
# 调用被装饰的函数
say_hello()
输出:
在函数执行之前
Hello!
在函数执行之后
2.2 装饰器的执行流程
@my_decorator
def say_hello():
...
等价于:
defsay_hello():
...
say_hello = my_decorator(say_hello)
2.3 原理解析
让我们逐步拆解上面的装饰器:
- 1. 定义阶段:Python 解释器读到
@my_decorator 时,会立即调用 my_decorator(say_hello),并将返回值重新赋值给 say_hello - 2. 调用阶段:当我们调用
say_hello() 时,实际调用的是 wrapper() 函数 - 3. 执行阶段:
wrapper() 内部调用了原始的 func()(即原 say_hello),并在其前后添加了额外逻辑
三、带参数的函数装饰器
3.1 装饰带参数的函数
上面的装饰器只能装饰无参数的函数。实际开发中,我们需要处理带参数的函数:
defmy_decorator(func):
defwrapper(*args, **kwargs):
print(f"调用函数 {func.__name__},参数: {args}, {kwargs}")
result = func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完毕,返回值: {result}")
return result
return wrapper
@my_decorator
defadd(a, b):
return a + b
@my_decorator
defgreet(name, age=20):
print(f"你好, {name}! 你今年 {age} 岁。")
# 测试
add(3, 5)
greet("小明", age=25)
输出:
调用函数 add,参数: (3, 5), {}
函数 add 执行完毕,返回值: 8
调用函数 greet,参数: ('小明',), {'age': 25}
你好, 小明! 你今年 25 岁。
函数 greet 执行完毕,返回值: None
3.2 关键点:*args 和 **kwargs
- •
**kwargs:接收所有关键字参数,打包成字典
四、带参数的装饰器
有时候我们需要给装饰器本身传递参数,比如指定重试次数、日志级别等。
4.1 语法结构
带参数的装饰器需要三层嵌套:
defrepeat(times):
"""装饰器工厂:控制函数重复执行次数"""
defdecorator(func):
defwrapper(*args, **kwargs):
for i inrange(times):
print(f"第 {i + 1} 次执行:")
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
defsay_hi():
print("Hi!")
say_hi()
输出:
第 1 次执行:
Hi!
第 2 次执行:
Hi!
第 3 次执行:
Hi!
4.2 执行流程解析
@repeat(3)
defsay_hi():
...
等价于:
defsay_hi():
...
say_hi = repeat(3)(say_hi)
# ↑ 先执行 ↑ 再执行
- 1.
repeat(3) 先执行,返回 decorator 函数 - 2.
decorator(say_hi) 再执行,返回 wrapper 函数
4.3 实战:带参数的重试装饰器
import time
defretry(max_retries=3, delay=1):
"""重试装饰器:函数失败时自动重试"""
defdecorator(func):
defwrapper(*args, **kwargs):
for attempt inrange(max_retries):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"第 {attempt + 1} 次尝试失败: {e}")
if attempt < max_retries - 1:
print(f"等待 {delay} 秒后重试...")
time.sleep(delay)
print("所有重试均失败")
return wrapper
return decorator
# 模拟一个可能失败的函数
import random
@retry(max_retries=3, delay=0.5)
deffetch_data():
if random.random() < 0.7: # 70% 概率失败
raise ConnectionError("网络连接失败")
return"数据获取成功!"
result = fetch_data()
五、functools.wraps 的作用
5.1 问题引入
使用装饰器后,原函数的元信息(函数名、文档字符串等)会丢失:
defmy_decorator(func):
defwrapper(*args, **kwargs):
"""wrapper 的文档字符串"""
return func(*args, **kwargs)
return wrapper
@my_decorator
defgreet(name):
"""向用户打招呼"""
print(f"Hello, {name}!")
print(f"函数名: {greet.__name__}") # 输出: wrapper
print(f"文档字符串: {greet.__doc__}") # 输出: wrapper 的文档字符串
5.2 解决方案:@wraps
from functools import wraps
defmy_decorator(func):
@wraps(func) # ← 关键:保留原函数的元信息
defwrapper(*args, **kwargs):
"""wrapper 的文档字符串"""
return func(*args, **kwargs)
return wrapper
@my_decorator
defgreet(name):
"""向用户打招呼"""
print(f"Hello, {name}!")
print(f"函数名: {greet.__name__}") # 输出: greet
print(f"文档字符串: {greet.__doc__}") # 输出: 向用户打招呼
5.3 最佳实践
所有装饰器都应该使用 @wraps,这是一个良好的编程习惯。它能:
六、多个装饰器的叠加
6.1 装饰器链
一个函数可以同时被多个装饰器装饰:
defdecorator_a(func):
@wraps(func)
defwrapper(*args, **kwargs):
print("[A] 执行前")
result = func(*args, **kwargs)
print("[A] 执行后")
return result
return wrapper
defdecorator_b(func):
@wraps(func)
defwrapper(*args, **kwargs):
print("[B] 执行前")
result = func(*args, **kwargs)
print("[B] 执行后")
return result
return wrapper
@decorator_a
@decorator_b
defhello():
print("Hello World!")
hello()
输出:
[A] 执行前
[B] 执行前
Hello World!
[B] 执行后
[A] 执行后
6.2 执行顺序
装饰器的执行顺序是从内到外(从下到上):
@decorator_a # 最外层,最后执行
@decorator_b # 内层,先执行
deffunc():
...
等价于:
func = decorator_a(decorator_b(func))
七、类装饰器
装饰器不仅可以是函数,也可以是类。
7.1 类作为装饰器
classCounter:
"""统计函数调用次数的装饰器"""
def__init__(self, func):
self.func = func
self.count = 0
def__call__(self, *args, **kwargs):
self.count += 1
print(f"第 {self.count} 次调用")
returnself.func(*args, **kwargs)
@Counter
defsay_hello():
print("Hello!")
say_hello()
say_hello()
say_hello()
print(f"总共调用了 {say_hello.count} 次")
输出:
第 1 次调用
Hello!
第 2 次调用
Hello!
第 3 次调用
Hello!
总共调用了 3 次
7.2 关键点
- • 类作为装饰器时,需要实现
__call__ 方法
八、实战案例
8.1 计时装饰器
import time
from functools import wraps
deftimer(func):
"""计算函数执行时间的装饰器"""
@wraps(func)
defwrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行耗时: {end - start:.6f} 秒")
return result
return wrapper
@timer
defslow_function():
"""模拟一个耗时操作"""
time.sleep(1)
return"完成"
slow_function()
8.2 日志装饰器
import logging
from functools import wraps
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
deflog(func):
"""记录函数调用的日志装饰器"""
@wraps(func)
defwrapper(*args, **kwargs):
logging.info(f"调用 {func.__name__}({', '.join(map(str, args))}, {kwargs})")
result = func(*args, **kwargs)
logging.info(f"{func.__name__} 返回 {result}")
return result
return wrapper
@log
defmultiply(a, b):
return a * b
multiply(6, 7)
8.3 缓存装饰器
from functools import wraps
defcache(func):
"""简单的缓存装饰器"""
cache_dict = {}
@wraps(func)
defwrapper(*args):
if args in cache_dict:
print(f"从缓存中获取: {args}")
return cache_dict[args]
result = func(*args)
cache_dict[args] = result
print(f"计算并缓存: {args}")
return result
return wrapper
@cache
deffibonacci(n):
"""计算斐波那契数列"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(5))
九、常见错误和注意事项
9.1 忘记返回 wrapper
# ❌ 错误:没有返回 wrapper
defbad_decorator(func):
defwrapper(*args, **kwargs):
print("before")
func(*args, **kwargs)
print("after")
# 这里缺少 return wrapper
# ✅ 正确
defgood_decorator(func):
defwrapper(*args, **kwargs):
print("before")
return func(*args, **kwargs)
return wrapper # ← 必须返回
9.2 忘记返回函数结果
# ❌ 错误:没有返回 result
defwrapper(*args, **kwargs):
func(*args, **kwargs) # 返回值被丢弃
# ✅ 正确
defwrapper(*args, **kwargs):
return func(*args, **kwargs) # 保留返回值
9.3 类方法装饰器中的 self
装饰类方法时,第一个参数是 self,要注意:
defmethod_logger(func):
@wraps(func)
defwrapper(self, *args, **kwargs):
print(f"调用 {self.__class__.__name__}.{func.__name__}")
return func(self, *args, **kwargs)
return wrapper
十、总结
| |
| |
| @decorator |
| 使用 *args, **kwargs 适配任意参数 |
| |
| |
| |
| 实现 __init__ 和 __call__ 方法 |
核心要点回顾
- 1. 装饰器 = 函数包装器,可以在不修改原函数的情况下添加功能
- 2.
@wraps 是必备,保留函数名和文档字符串 - 3.
*args, **kwargs 是标配,让装饰器可以装饰任何函数
十一、练习题
练习 1:编写权限检查装饰器
编写一个装饰器,检查用户是否有权限调用某函数。如果用户名不在允许列表中,打印 "无权限"。
练习 2:编写性能监控装饰器
编写一个装饰器,统计并打印函数的执行时间,如果超过指定阈值则发出警告。
练习 3:编写 HTML 标签装饰器
编写一个装饰器,将函数的字符串返回值包裹在指定的 HTML 标签中(如 <p>, <div>)。
# 期望效果
@html_tag("p")
defget_text():
return"Hello World"
print(get_text()) # 输出: <p>Hello World</p>
参考答案
练习 1 参考答案from functools import wraps
defrequire_permission(allowed_users):
defdecorator(func):
@wraps(func)
defwrapper(user, *args, **kwargs):
if user notin allowed_users:
print(f"用户 '{user}' 无权限调用 {func.__name__}")
returnNone
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_permission(["admin", "manager"])
defdelete_record(user, record_id):
print(f"用户 {user} 删除了记录 {record_id}")
delete_record("admin", 101)
delete_record("guest", 102)
练习 2 参考答案import time
from functools import wraps
deftime_limit(max_seconds):
defdecorator(func):
@wraps(func)
defwrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
if elapsed > max_seconds:
print(f"⚠️ 警告: {func.__name__} 耗时 {elapsed:.2f}s,超过阈值 {max_seconds}s")
else:
print(f"✅ {func.__name__} 耗时 {elapsed:.4f}s")
return result
return wrapper
return decorator
练习 3 参考答案from functools import wraps
defhtml_tag(tag_name):
defdecorator(func):
@wraps(func)
defwrapper(*args, **kwargs):
result = func(*args, **kwargs)
returnf"<{tag_name}>{result}</{tag_name}>"
return wrapper
return decorator
@html_tag("p")
defget_text():
return"Hello World"
print(get_text()) # <p>Hello World</p>
🎉 恭喜你完成了第24天的学习!装饰器是 Python 中非常重要的高级特性,掌握它将让你的代码更加优雅和模块化。
📌 下一天预告:第25天 —— Python中模块与包详解