Python 装饰器新手入门与实战指南
本文面向零基础读者,从最基础的概念讲起,手把手带你掌握 Python 装饰器的核心用法与实战技巧。
WEB项目地址:演示地址
① 装饰器核心概念与生活化类比解析
装饰器是什么?
装饰器(Decorator)是 Python 中一种强大的特性,它允许我们在不修改原函数代码的情况下,给函数动态地添加新功能。装饰器本质上是一个函数,它接受一个函数作为参数,并返回一个新的函数。
生活化类比:手机壳
想象一下你买了一部手机(原函数),手机本身的功能就是打电话、发微信。后来你给手机套了一个手机壳(装饰器),手机还是那个手机,功能一点没变,但多了防摔、防滑的新功能。装饰器就是这个“手机壳”——在不改变手机本身的前提下,给它增加了额外功能。
装饰器的两个核心基础
在学习装饰器之前,必须先理解两个概念:
① 函数是“一等公民”
在 Python 中,函数和整数、字符串一样,可以:
- 赋值给变量
- 作为参数传递给其他函数
- 作为返回值从其他函数中返回
- 在函数内部嵌套定义
② 闭包(Closure)
闭包简单说就是:在一个外层函数中定义内层函数,内层函数引用外层函数的变量,并且外层函数返回内层函数。即使外层函数执行完了,内层函数依然能“记住”外层函数的变量。装饰器的核心结构就是基于闭包实现的。
装饰器的核心价值
装饰器体现了“对扩展开放,对修改关闭”的开闭原则。它让你能把日志记录、性能计时、权限校验这些“通用逻辑”抽出来,一次编写、到处复用,让业务代码保持干净整洁。
② 本地开发环境准备与版本检查
装饰器是 Python 的内置特性,不需要安装任何额外库,只要有 Python 环境即可。
检查 Python 版本
打开终端(Windows 用 CMD 或 PowerShell,Mac/Linux 用 Terminal),输入:
python--version# 或python3--version如果显示类似Python 3.8.0或更高版本,说明环境已就绪。建议使用Python 3.6 及以上版本,装饰器语法在这些版本中完全支持。
如果没有 Python
- Windows:访问 python.org 下载安装包,运行安装时务必勾选“Add Python to PATH”
- Mac/Linux:使用系统包管理器安装,如
sudo apt install python3 python3-pip
推荐编辑器
对于新手,推荐使用VS Code或PyCharm社区版,它们对 Python 有良好的语法高亮和调试支持。
③ 函数作为对象的基础操作演示
在写装饰器之前,先用几个小例子感受一下“函数也是对象”:
示例1:函数赋值给变量
defadd(a,b):returna+b# 把函数赋值给变量(注意:不加括号,不是调用)func=add# 通过变量调用函数print(func(1,2))# 输出:3函数add被赋值给变量func后,func和add指向同一个函数对象。
示例2:函数作为参数
defgreet(name):returnf"Hello,{name}!"defrun_with_log(func,arg):print("准备执行函数...")result=func(arg)print("函数执行完毕")returnresultprint(run_with_log(greet,"Alice"))# 输出:# 准备执行函数...# Hello, Alice!# 函数执行完毕示例3:函数作为返回值(闭包雏形)
defouter(fixed_num):definner(dynamic_num):returnfixed_num+dynamic_numreturninner# 返回内层函数add5=outer(5)# 生成“固定加5”的函数add10=outer(10)# 生成“固定加10”的函数print(add5(3))# 输出:8print(add10(4))# 输出:14这里的inner就是一个闭包——它“记住”了外层函数传入的fixed_num。
④ 编写第一个无参数装饰器代码
有了前面的基础,现在正式写一个装饰器。
最简装饰器模板
defmy_decorator(func):defwrapper():print("函数执行前")func()print("函数执行后")returnwrapper@my_decoratordefsay_hello():print("Hello!")say_hello()# 输出:# 函数执行前# Hello!# 函数执行后拆解理解
@my_decorator这行代码实际上就是say_hello = my_decorator(say_hello)的简写。整个过程是:
my_decorator接收say_hello函数作为参数- 在
my_decorator内部定义了wrapper函数,在调用原函数前后添加了额外逻辑 my_decorator返回wrapper函数say_hello被替换成了wrapper
用 @ 语法糖 vs 手动调用
这两种写法完全等价:
# 方式一:使用 @ 语法糖(推荐)@my_decoratordefsay_hello():print("Hello!")# 方式二:手动调用(不推荐,仅用于理解原理)defsay_hello():print("Hello!")say_hello=my_decorator(say_hello)⑤ 处理带参数函数的装饰器写法
上面的装饰器只能装饰无参数的函数。如果被装饰的函数有参数,wrapper必须能接收并传递这些参数。
使用 *args 和 **kwargs
defmy_decorator(func):defwrapper(*args,**kwargs):# 接收任意参数print("函数执行前")result=func(*args,**kwargs)# 原封不动传过去print("函数执行后")returnresultreturnwrapper@my_decoratordefadd(a,b):returna+b@my_decoratordefgreet(name,greeting="Hello"):returnf"{greeting},{name}!"print(add(3,5))# 输出:8(带提示信息)print(greet("Bob"))# 输出:Hello, Bob!(带提示信息)*args接收所有位置参数,**kwargs接收所有关键字参数。这样装饰器就能通用于任何函数了。
⑥ 保留原函数元信息的标准实践
问题:装饰器会“偷走”函数身份
如果不做特殊处理,被装饰后的函数会丢失原有的名称和文档字符串:
defbad_decorator(func):defwrapper():func()returnwrapper@bad_decoratordefhello():"""我是文档"""passprint(hello.__name__)# 输出:wrapper(不是 hello)print(hello.__doc__)# 输出:None(文档丢了)调试的时候看到wrapper这个名字,根本不知道它原来是哪个函数。
解决方案:@functools.wraps
使用functools.wraps可以将原始函数的元信息(名称、文档字符串、参数列表等)复制到包装函数上。
fromfunctoolsimportwrapsdefgood_decorator(func):@wraps(func)# 关键:保留原函数元信息defwrapper(*args,**kwargs):print("执行前")result=func(*args,**kwargs)print("执行后")returnresultreturnwrapper@good_decoratordefhello():"""我是文档"""passprint(hello.__name__)# 输出:helloprint(hello.__doc__)# 输出:我是文档这是一个必须养成的好习惯——所有自己写的装饰器都应该加上@wraps(func)。
⑦ 多个装饰器叠加使用的执行顺序
Python 允许对同一个函数应用多个装饰器。
执行顺序规则
装饰器的应用顺序是从下往上(从内到外),但执行时外层先执行。
看代码理解:
defdecorator1(func):defwrapper():print("装饰器1 前置")func()print("装饰器1 后置")returnwrapperdefdecorator2(func):defwrapper():print("装饰器2 前置")func()print("装饰器2 后置")returnwrapper@decorator1@decorator2deftarget():print("目标函数")target()# 输出:# 装饰器1 前置# 装饰器2 前置# 目标函数# 装饰器2 后置# 装饰器1 后置理解顺序
@decorator1在最外层(离函数最远),@decorator2在里层(离函数最近)。等价于:
target=decorator1(decorator2(target))所以执行时:decorator1 的前置 → decorator2 的前置 → 原函数 → decorator2 的后置 → decorator1 的后置。
⑧ 类方法装饰器与静态方法应用
内置的类相关装饰器
Python 提供了几个内置装饰器用于类方法:
| 装饰器 | 作用 |
|---|---|
@staticmethod | 定义静态方法,不需要实例即可调用 |
@classmethod | 定义类方法,第一个参数是类本身 |
@property | 将方法变为属性访问方式 |
示例
classMyClass:@staticmethoddefstatic_method():"""不需要实例,也不需要类,就像普通函数"""print("这是静态方法")@classmethoddefclass_method(cls):"""第一个参数是类本身"""print(f"这是类方法,类名:{cls.__name__}")@propertydeffull_name(self):return"张三"# 调用MyClass.static_method()# 不需要实例MyClass.class_method()# 不需要实例obj=MyClass()print(obj.full_name)# 像属性一样访问,不加括号自定义装饰器应用于类方法
如果想给类方法写自定义装饰器,要确保自定义装饰器在@classmethod或@staticmethod之前:
fromfunctoolsimportwrapsimporttimedeftimer(func):@wraps(func)defwrapper(*args,**kwargs):start=time.time()result=func(*args,**kwargs)print(f"{func.__name__}耗时:{time.time()-start:.2f}秒")returnresultreturnwrapperclassCalculator:@timer# 自定义装饰器在前@staticmethoddefslow_calc():time.sleep(0.5)return42print(Calculator.slow_calc())# 输出:slow_calc 耗时:0.50秒# 42⑨ 日志记录与性能计时实战案例
案例1:计时装饰器
测量函数执行时间是最常用的装饰器场景之一:
importtimefromfunctoolsimportwrapsdeftimer(func):@wraps(func)defwrapper(*args,**kwargs):start=time.time()result=func(*args,**kwargs)end=time.time()print(f"{func.__name__}耗时:{end-start:.4f}秒")returnresultreturnwrapper@timerdefslow_function():time.sleep(1)return"完成"print(slow_function())# 输出:slow_function 耗时:1.0012秒# 完成案例2:日志记录装饰器
记录函数的调用信息和返回值,便于调试:
fromfunctoolsimportwrapsdefdebug(func):@wraps(func)defwrapper(*args,**kwargs):# 格式化参数args_str=', '.join(repr(arg)forarginargs)kwargs_str=', '.join(f"{k}={v!r}"fork,vinkwargs.items())all_args=', '.join(filter(None,[args_str,kwargs_str]))print(f"调用{func.__name__}({all_args})")result=func(*args,**kwargs)print(f"{func.__name__}返回{result!r}")returnresultreturnwrapper@debugdefadd_numbers(a,b,multiply_by=1):return(a+b)*multiply_by add_numbers(5,3,multiply_by=2)# 输出:# 调用 add_numbers(5, 3, multiply_by=2)# add_numbers 返回 16案例3:重试装饰器(带参数)
带参数的装饰器通过三层嵌套实现:
fromfunctoolsimportwrapsimporttimedefretry(max_attempts=3,delay=1):defdecorator(func):@wraps(func)defwrapper(*args,**kwargs):forattemptinrange(1,max_attempts+1):try:returnfunc(*args,**kwargs)exceptExceptionase:ifattempt==max_attempts:raiseprint(f"第{attempt}次失败,{delay}秒后重试...")time.sleep(delay)returnwrapperreturndecorator@retry(max_attempts=3,delay=0.5)defunstable_request():importrandomifrandom.random()<0.7:raiseValueError("请求失败")return"成功!"⑩ 常见缩进错误与调用异常排查
错误1:忘记返回 wrapper
defbad_decorator(func):defwrapper():func()# 忘记 return wrapper——返回 None!@bad_decoratordefhello():print("Hello")hello()# TypeError: 'NoneType' object is not callable解决方法:始终确保装饰器返回 wrapper 函数。
错误2:忘记传递 *args 和 **kwargs
defbad_decorator(func):defwrapper():# 没有参数func()# 原函数有参数时会报错returnwrapper@bad_decoratordefadd(a,b):returna+b add(1,2)# TypeError: wrapper() takes 0 positional arguments but 2 were given解决方法:wrapper 必须使用*args, **kwargs接收并传递所有参数。
错误3:缩进问题
Python 对缩进非常敏感。装饰器的@符号和函数定义def必须在同一缩进级别:
# ❌ 错误:@ 缩进和 def 不一致@my_decoratordefhello():pass# ✅ 正确@my_decoratordefhello():pass错误4:忘记返回原函数的结果
defbad_decorator(func):defwrapper(*args,**kwargs):func(*args,**kwargs)# 没有 return,返回值丢失returnwrapper@bad_decoratordefadd(a,b):returna+bprint(add(1,2))# 输出:None解决方法:调用原函数后一定要return它的结果。
错误5:装饰器缺少 @wraps
不使用@wraps会导致函数元信息丢失,调试困难。养成好习惯:所有装饰器都加上@wraps(func)。
总结
| 知识点 | 要点 |
|---|---|
| 装饰器本质 | 接收函数、返回函数的函数(闭包) |
| 无参数装饰器 | 两层嵌套:外层接收 func,内层 wrapper |
| 带参数装饰器 | 三层嵌套:最外层接收装饰器参数 |
| 保留元信息 | 始终使用@wraps(func) |
| 多装饰器顺序 | 从下往上应用,外层先执行 |
| 通用性 | wrapper 使用*args, **kwargs |
| 返回值 | 记得return原函数的执行结果 |
装饰器是 Python 中最优雅的特性之一。从最简单的计时器开始,逐步尝试写日志、重试、缓存等装饰器,你会发现自己代码的复用性和整洁度都会有质的提升。