30天入门Python(基础篇)——第24天:Python中的装饰器详解
📅学习日期:第24天 | ⏱️预计用时:60分钟 | 📊难度等级:⭐⭐⭐⭐
🎯 学习目标
- 理解装饰器的本质和作用
- 掌握装饰器的基本语法和使用方法
- 学会编写带参数的装饰器
- 理解
functools.wraps的作用 - 掌握类装饰器的使用方法
- 能够编写多个装饰器叠加的复杂场景
一、什么是装饰器?
1.1 装饰器的概念
装饰器(Decorator)是 Python 中一个非常强大且优雅的特性。从本质上讲,装饰器就是一个函数,它接受一个函数作为参数,返回一个新的函数。装饰器允许我们在不修改原有函数代码的情况下,为其添加额外的功能。
打个比方:装饰器就像给手机套上一个手机壳。手机本身的功能不变,但手机壳给它增加了防摔、美观等额外特性。
1.2 装饰器的设计原则
装饰器遵循开放-封闭原则(Open-Closed Principle):
- 对扩展开放:可以给函数添加新功能
- 对修改封闭:不需要修改原函数的代码
二、函数装饰器的基础
2.1 最简单的装饰器
defmy_decorator(func):"""一个简单的装饰器"""defwrapper():print("在函数执行之前")func()print("在函数执行之后")returnwrapper@my_decoratordefsay_hello():print("Hello!")# 调用被装饰的函数say_hello()输出:
在函数执行之前 Hello! 在函数执行之后2.2 装饰器的执行流程
@my_decorator def say_hello(): ...等价于:
defsay_hello():...say_hello=my_decorator(say_hello)2.3 原理解析
让我们逐步拆解上面的装饰器:
- 定义阶段:Python 解释器读到
@my_decorator时,会立即调用my_decorator(say_hello),并将返回值重新赋值给say_hello - 调用阶段:当我们调用
say_hello()时,实际调用的是wrapper()函数 - 执行阶段:
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}")returnresultreturnwrapper@my_decoratordefadd(a,b):returna+b@my_decoratordefgreet(name,age=20):print(f"你好,{name}! 你今年{age}岁。")# 测试add(3,5)greet("小明",age=25)输出:
调用函数 add,参数: (3, 5), {} 函数 add 执行完毕,返回值: 8 调用函数 greet,参数: ('小明',), {'age': 25} 你好, 小明! 你今年 25 岁。 函数 greet 执行完毕,返回值: None3.2 关键点:*args和**kwargs
*args:接收所有位置参数,打包成元组**kwargs:接收所有关键字参数,打包成字典- 这种写法使装饰器可以适配任意参数签名的函数
四、带参数的装饰器
有时候我们需要给装饰器本身传递参数,比如指定重试次数、日志级别等。
4.1 语法结构
带参数的装饰器需要三层嵌套:
defrepeat(times):"""装饰器工厂:控制函数重复执行次数"""defdecorator(func):defwrapper(*args,**kwargs):foriinrange(times):print(f"第{i+1}次执行:")result=func(*args,**kwargs)returnresultreturnwrapperreturndecorator@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)# ↑ 先执行 ↑ 再执行repeat(3)先执行,返回decorator函数decorator(say_hi)再执行,返回wrapper函数- 最终
say_hi指向wrapper
4.3 实战:带参数的重试装饰器
importtimedefretry(max_retries=3,delay=1):"""重试装饰器:函数失败时自动重试"""defdecorator(func):defwrapper(*args,**kwargs):forattemptinrange(max_retries):try:returnfunc(*args,**kwargs)exceptExceptionase:print(f"第{attempt+1}次尝试失败:{e}")ifattempt<max_retries-1:print(f"等待{delay}秒后重试...")time.sleep(delay)print("所有重试均失败")returnwrapperreturndecorator# 模拟一个可能失败的函数importrandom@retry(max_retries=3,delay=0.5)deffetch_data():ifrandom.random()<0.7:# 70% 概率失败raiseConnectionError("网络连接失败")return"数据获取成功!"result=fetch_data()五、functools.wraps的作用
5.1 问题引入
使用装饰器后,原函数的元信息(函数名、文档字符串等)会丢失:
defmy_decorator(func):defwrapper(*args,**kwargs):"""wrapper 的文档字符串"""returnfunc(*args,**kwargs)returnwrapper@my_decoratordefgreet(name):"""向用户打招呼"""print(f"Hello,{name}!")print(f"函数名:{greet.__name__}")# 输出: wrapperprint(f"文档字符串:{greet.__doc__}")# 输出: wrapper 的文档字符串5.2 解决方案:@wraps
fromfunctoolsimportwrapsdefmy_decorator(func):@wraps(func)# ← 关键:保留原函数的元信息defwrapper(*args,**kwargs):"""wrapper 的文档字符串"""returnfunc(*args,**kwargs)returnwrapper@my_decoratordefgreet(name):"""向用户打招呼"""print(f"Hello,{name}!")print(f"函数名:{greet.__name__}")# 输出: greetprint(f"文档字符串:{greet.__doc__}")# 输出: 向用户打招呼5.3 最佳实践
所有装饰器都应该使用@wraps,这是一个良好的编程习惯。它能:
- 保留原函数名
__name__ - 保留原函数文档
__doc__ - 保留原函数的其他元信息
- 对调试、日志、自省等场景非常重要
六、多个装饰器的叠加
6.1 装饰器链
一个函数可以同时被多个装饰器装饰:
defdecorator_a(func):@wraps(func)defwrapper(*args,**kwargs):print("[A] 执行前")result=func(*args,**kwargs)print("[A] 执行后")returnresultreturnwrapperdefdecorator_b(func):@wraps(func)defwrapper(*args,**kwargs):print("[B] 执行前")result=func(*args,**kwargs)print("[B] 执行后")returnresultreturnwrapper@decorator_a@decorator_bdefhello():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=0def__call__(self,*args,**kwargs):self.count+=1print(f"第{self.count}次调用")returnself.func(*args,**kwargs)@Counterdefsay_hello():print("Hello!")say_hello()say_hello()say_hello()print(f"总共调用了{say_hello.count}次")输出:
第 1 次调用 Hello! 第 2 次调用 Hello! 第 3 次调用 Hello! 总共调用了 3 次7.2 关键点
- 类作为装饰器时,需要实现
__call__方法 __init__接收被装饰的函数__call__在每次调用函数时执行
八、实战案例
8.1 计时装饰器
importtimefromfunctoolsimportwrapsdeftimer(func):"""计算函数执行时间的装饰器"""@wraps(func)defwrapper(*args,**kwargs):start=time.time()result=func(*args,**kwargs)end=time.time()print(f"{func.__name__}执行耗时:{end-start:.6f}秒")returnresultreturnwrapper@timerdefslow_function():"""模拟一个耗时操作"""time.sleep(1)return"完成"slow_function()8.2 日志装饰器
importloggingfromfunctoolsimportwraps 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}")returnresultreturnwrapper@logdefmultiply(a,b):returna*b multiply(6,7)8.3 缓存装饰器
fromfunctoolsimportwrapsdefcache(func):"""简单的缓存装饰器"""cache_dict={}@wraps(func)defwrapper(*args):ifargsincache_dict:print(f"从缓存中获取:{args}")returncache_dict[args]result=func(*args)cache_dict[args]=resultprint(f"计算并缓存:{args}")returnresultreturnwrapper@cachedeffibonacci(n):"""计算斐波那契数列"""ifn<=1:returnnreturnfibonacci(n-1)+fibonacci(n-2)print(fibonacci(5))九、常见错误和注意事项
9.1 忘记返回 wrapper
# ❌ 错误:没有返回 wrapperdefbad_decorator(func):defwrapper(*args,**kwargs):print("before")func(*args,**kwargs)print("after")# 这里缺少 return wrapper# ✅ 正确defgood_decorator(func):defwrapper(*args,**kwargs):print("before")returnfunc(*args,**kwargs)returnwrapper# ← 必须返回9.2 忘记返回函数结果
# ❌ 错误:没有返回 resultdefwrapper(*args,**kwargs):func(*args,**kwargs)# 返回值被丢弃# ✅ 正确defwrapper(*args,**kwargs):returnfunc(*args,**kwargs)# 保留返回值9.3 类方法装饰器中的 self
装饰类方法时,第一个参数是self,要注意:
defmethod_logger(func):@wraps(func)defwrapper(self,*args,**kwargs):print(f"调用{self.__class__.__name__}.{func.__name__}")returnfunc(self,*args,**kwargs)returnwrapper十、总结
| 知识点 | 关键内容 |
|---|---|
| 装饰器本质 | 接受函数作为参数,返回新函数的高阶函数 |
| 基本语法 | @decorator放在函数定义上方 |
| 参数处理 | 使用*args, **kwargs适配任意参数 |
| 装饰器参数 | 需要三层嵌套函数 |
| 元信息保留 | 使用@wraps(func) |
| 多个装饰器 | 从下到上(从内到外)执行 |
| 类装饰器 | 实现__init__和__call__方法 |
核心要点回顾
- 装饰器 = 函数包装器,可以在不修改原函数的情况下添加功能
@wraps是必备,保留函数名和文档字符串*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 参考答案fromfunctoolsimportwrapsdefrequire_permission(allowed_users):defdecorator(func):@wraps(func)defwrapper(user,*args,**kwargs):ifusernotinallowed_users:print(f"用户 '{user}' 无权限调用{func.__name__}")returnNonereturnfunc(user,*args,**kwargs)returnwrapperreturndecorator@require_permission(["admin","manager"])defdelete_record(user,record_id):print(f"用户{user}删除了记录{record_id}")delete_record("admin",101)delete_record("guest",102)练习 2 参考答案importtimefromfunctoolsimportwrapsdeftime_limit(max_seconds):defdecorator(func):@wraps(func)defwrapper(*args,**kwargs):start=time.time()result=func(*args,**kwargs)elapsed=time.time()-startifelapsed>max_seconds:print(f"⚠️ 警告:{func.__name__}耗时{elapsed:.2f}s,超过阈值{max_seconds}s")else:print(f"✅{func.__name__}耗时{elapsed:.4f}s")returnresultreturnwrapperreturndecorator练习 3 参考答案fromfunctoolsimportwrapsdefhtml_tag(tag_name):defdecorator(func):@wraps(func)defwrapper(*args,**kwargs):result=func(*args,**kwargs)returnf"<{tag_name}>{result}</{tag_name}>"returnwrapperreturndecorator@html_tag("p")defget_text():return"Hello World"print(get_text())# <p>Hello World</p>🎉 恭喜你完成了第24天的学习!装饰器是 Python 中非常重要的高级特性,掌握它将让你的代码更加优雅和模块化。
📌下一天预告:第25天 —— Python中模块与包详解