Python 装饰器(Decorator)是一种高级语法,用于在不修改原函数代码的前提下,动态扩展函数或类的功能。它本质上是一个接收函数 / 类作为参数,并返回新函数 / 类的可调用对象,是函数式编程的典型应用。
1、核心原理
装饰器的核心依赖于 Python 的两个特性:
- 函数:函数可以作为参数传递、作为返回值返回、赋值给变量。
- 闭包:嵌套函数可以访问外层函数的变量和参数(即使外层函数已执行完毕)。
1.1 装饰器的基本结构
一个最简单的装饰器由外层函数(接收原函数) 和内层函数(包装原函数) 组成,最终返回内层函数:
def decorator(func): # 外层函数:接收原函数作为参数def wrapper(*args, **kwargs): # 内层函数:包装原函数(*args, **kwargs 兼容任意参数)print("装饰器逻辑:执行前") # 扩展功能(执行前)result = func(*args, **kwargs) # 调用原函数print("装饰器逻辑:执行后") # 扩展功能(执行后)return result # 返回原函数结果return wrapper # 返回内层函数# 使用装饰器(语法糖:@装饰器名)
@decorator
def target_func():print("原函数逻辑")# 调用被装饰的函数
target_func()
执行结果:
装饰器逻辑:执行前
原函数逻辑
装饰器逻辑:执行后
说明:@decorator 等价于 target_func = decorator(target_func),即原函数被装饰器返回的 wrapper 函数替换。
1.2 装饰器的执行时机
装饰器在模块加载时(导入时) 就会执行,而非函数调用时。例如:
print("模块加载中...")def decorator(func):print(f"装饰器执行:装饰 {func.__name__}")return func@decorator
def func1():pass@decorator
def func2():passprint("模块加载完成")
执行结果(先执行装饰器,再执行后续代码):
模块加载中...
装饰器执行:装饰 func1
装饰器执行:装饰 func2
模块加载完成
2、进阶用法
2.1 带参数的装饰器
若需要给装饰器传递参数(如日志的级别、缓存的过期时间),需在外层再套一层 “参数接收函数”:
def log_decorator(level): # 最外层:接收装饰器参数def decorator(func): # 中间层:接收原函数def wrapper(*args, **kwargs):print(f"[{level}] 函数 {func.__name__} 开始执行") # 使用装饰器参数result = func(*args, **kwargs)print(f"[{level}] 函数 {func.__name__} 执行结束")return resultreturn wrapperreturn decorator# 使用带参数的装饰器
@log_decorator(level="INFO")
def add(a, b):return a + bprint(add(1, 2))
执行结果:
[INFO] 函数 add 开始执行
[INFO] 函数 add 执行结束
3
2.2 类装饰器
类也可以作为装饰器,核心是实现 __call__ 方法(使类实例可调用):
class TimerDecorator:def __init__(self, func): # 初始化时接收原函数self.func = funcdef __call__(self, *args, **kwargs): # 调用时执行装饰逻辑import timestart = time.time()result = self.func(*args, **kwargs) # 调用原函数end = time.time()print(f"函数 {self.func.__name__} 执行耗时:{end - start:.2f}s")return result# 使用类装饰器
@TimerDecorator
def slow_func(seconds):import timetime.sleep(seconds) # 模拟耗时操作slow_func(1) # 执行被装饰的函数
执行结果:
函数 slow_func 执行耗时:1.00s
2.3 装饰器的嵌套
多个装饰器可以叠加使用,嵌套装饰器的核心是装饰顺序和调用顺序的区别,理解这两个顺序是掌握嵌套装饰器的关键。
# 定义两个装饰器
def decorator_a(func):def wrapper(*args, **kwargs):print("装饰器 A 开始")result = func(*args, **kwargs)print("装饰器 A 结束")return resultreturn wrapperdef decorator_b(func):def wrapper(*args, **kwargs):print("装饰器 B 开始")result = func(*args, **kwargs)print("装饰器 B 结束")return resultreturn wrapper# 嵌套应用装饰器
@decorator_a
@decorator_b
def target():print("执行目标函数")# 调用目标函数
target()
执行结果:
装饰器 A 开始
装饰器 B 开始
执行目标函数
装饰器 B 结束
装饰器 A 结束
2.3.1 装饰顺序:从下到上(“包裹顺序”)
装饰器的应用过程发生在函数定义时,本质是对函数的 “层层包裹”。对于上面的例子:
@decorator_a
@decorator_b
def target(): ...
等价于手动执行:
# 第一步:用 decorator_b 装饰原始 target,得到新函数 wrapper_b
wrapper_b = decorator_b(target)
# 第二步:用 decorator_a 装饰 wrapper_b,得到最终函数 wrapper_a
target = decorator_a(wrapper_b)
即:先执行下方的装饰器,再执行上方的装饰器,最终得到的 target 是最外层装饰器(decorator_a)返回的 wrapper 函数。
2.3.2 调用顺序:从外到内(“执行顺序”)
当调用被装饰的函数(如 target())时,执行流程是从最外层装饰器的 wrapper 开始,逐层进入内层,最后执行原始函数,再逐层返回。
以上面的 target() 调用为例,执行流程拆解:
- 调用
target()实际是调用decorator_a返回的wrapper_a; wrapper_a中先执行自身逻辑(print("装饰器 A 开始")),然后调用func(此时func是decorator_b返回的wrapper_b);wrapper_b中先执行自身逻辑(print("装饰器 B 开始")),然后调用func(此时func是原始target函数);- 执行原始
target函数(print("执行目标函数")); - 原始函数执行完毕,返回结果给
wrapper_b,wrapper_b执行后续逻辑(print("装饰器 B 结束")); wrapper_b执行完毕,返回结果给wrapper_a,wrapper_a执行后续逻辑(print("装饰器 A 结束"));- 最终结果返回给调用者。
简言之:调用时先执行外层装饰器的 “前置逻辑”,再执行内层装饰器的 “前置逻辑”,然后是原始函数,最后按相反顺序执行各装饰器的 “后置逻辑”。
2.4 保留原函数元信息
装饰器默认会覆盖原函数的元信息(如 __name__、__doc__),需使用 functools.wraps 修复:
import functoolsdef bad_decorator(func):def wrapper():func()return wrapper # 未保留元信息def good_decorator(func):@functools.wraps(func) # 保留原函数元信息def wrapper():func()return wrapper@bad_decorator
def f1():"""f1 的文档字符串"""pass@good_decorator
def f2():"""f2 的文档字符串"""passprint(f1.__name__) # 输出:wrapper(错误)
print(f1.__doc__) # 输出:None(错误)
print(f2.__name__) # 输出:f2(正确)
print(f2.__doc__) # 输出:f2 的文档字符串(正确)
2.5 装饰类的方法
2.5.1 装饰实例方法
实例方法是类中最常见的方法,第一个参数默认是 self(指向实例本身)。装饰实例方法时,装饰器需要正确传递 self 及其他参数。
用装饰器记录实例方法的调用日志
import functools# 定义装饰器:记录方法调用的参数、返回值
def log_instance_method(func):@functools.wraps(func) # 保留原方法的元信息(如 __name__、__doc__)def wrapper(self, *args, **kwargs):# 打印实例信息、方法名、参数print(f"[日志] 实例 {self} 调用方法 {func.__name__},参数:{args},关键字参数:{kwargs}")# 调用原始方法(注意传递 self)result = func(self, *args, **kwargs)# 打印返回值print(f"[日志] 方法 {func.__name__} 返回:{result}")return resultreturn wrapper# 定义类,用装饰器装饰实例方法
class User:def __init__(self, name):self.name = name # 实例属性# 用装饰器装饰实例方法@log_instance_methoddef greet(self, prefix):"""向用户打招呼"""return f"{prefix},我是 {self.name}"# 测试
user = User("Alice")
user.greet("你好")
执行结果:
[日志] 实例 <__main__.User object at 0x102b5a4d0> 调用方法 greet,参数:('你好',),关键字参数:{}
[日志] 方法 greet 返回:你好,我是 Alice
关键说明:
- 装饰器的
wrapper函数第一个参数必须是self(或用*args兼容),否则无法传递实例引用; func(self, *args, **kwargs)确保原始方法能正确接收self及其他参数;functools.wraps(func)保留原方法的元信息(如user.greet.__name__仍为greet,而非wrapper)。
2.5.2 装饰类方法
类方法用 @classmethod 装饰,第一个参数默认是 cls(指向类本身)。装饰类方法时,需注意装饰器与 @classmethod 的顺序,以及 cls 参数的传递。
用装饰器验证类方法的参数
import functools# 定义装饰器:检查类方法的参数是否为正数
def validate_positive(func):@functools.wraps(func)def wrapper(cls, *args, **kwargs):# 检查所有位置参数是否为正数for arg in args:if not isinstance(arg, (int, float)) or arg <= 0:raise ValueError(f"类方法 {func.__name__} 接收无效参数:{arg}(必须为正数)")# 调用原始类方法(传递 cls)return func(cls, *args, **kwargs)return wrapper# 定义类,用装饰器装饰类方法
class MathUtils:# 注意装饰器顺序:先应用功能装饰器,再用 @classmethod(从下到上装饰)@classmethod@validate_positivedef multiply(cls, a, b):"""计算两个正数的乘积(类方法)"""return a * b# 测试
print(MathUtils.multiply(3, 4)) # 正常调用:输出 12
MathUtils.multiply(-2, 5) # 触发验证失败:ValueError
执行结果:
12
ValueError: 类方法 multiply 接收无效参数:-2(必须为正数)
关键说明:
- 装饰器顺序:
@classmethod必须放在功能装饰器的下方(即先被装饰),因为@classmethod会将方法转换为类方法,功能装饰器需要装饰转换后的方法; wrapper函数第一个参数是cls,确保原始类方法能接收类引用;- 类方法操作的是类本身(如
cls.__name__),而非实例,装饰器可基于类属性做校验。
2.5.3 装饰静态方法
静态方法用 @staticmethod 装饰,无默认参数(与普通函数类似)。装饰静态方法时,处理逻辑与装饰普通函数一致。
用装饰器记录静态方法的执行时间
import functools
import time# 定义装饰器:记录方法执行时间
def timer(func):@functools.wraps(func)def wrapper(*args, **kwargs):start = time.time()result = func(*args, **kwargs) # 静态方法无 self/cls,直接传递参数end = time.time()print(f"[计时] 静态方法 {func.__name__} 执行耗时:{end - start:.4f} 秒")return resultreturn wrapper# 定义类,用装饰器装饰静态方法
class StringUtils:# 装饰器顺序:@staticmethod 在下,功能装饰器在上@staticmethod@timerdef reverse(s):"""反转字符串(静态方法)"""time.sleep(0.1) # 模拟耗时操作return s[::-1]# 测试
print(StringUtils.reverse("hello")) # 输出:olleh
执行结果:
[计时] 静态方法 reverse 执行耗时:0.1002 秒
olleh
关键说明:
- 静态方法无
self或cls,装饰器的wrapper函数直接用*args, **kwargs接收参数即可; - 装饰器顺序与类方法类似:
@staticmethod在下,功能装饰器在上,确保装饰的是静态方法。
3、装饰器的应用场景
-
日志记录:自动记录函数的调用参数、返回值、执行时间。
-
权限验证:调用函数前检查用户权限(如登录状态)。
def login_required(func):@functools.wraps(func)def wrapper(user, *args, **kwargs):if not user.is_login:raise PermissionError("请先登录")return func(user, *args, **kwargs)return wrapper@login_required def pay(user, amount):print(f"{user.name} 支付 {amount} 元") -
缓存结果:缓存函数的计算结果,避免重复计算。
from functools import lru_cache@lru_cache(maxsize=128) def expensive_calculation(x, y):# 模拟耗时计算time.sleep(1)return x * y + x**2 + y**2 # 第一次调用会计算 result1 = expensive_calculation(10, 20) # 第二次相同参数的调用会直接从缓存返回 result2 = expensive_calculation(10, 20) -
异常处理:统一捕获函数执行中的异常,避免程序崩溃。
import functools from typing import Callable, Any, Type, Tuple, Optionaldef exception_handler(exceptions: Tuple[Type[Exception], ...] = (Exception,),default: Any = None,re_raise: bool = False,error_msg: Optional[str] = None ) -> Callable:"""异常处理装饰器:捕获指定异常并统一处理:param exceptions: 需要捕获的异常类型(元组),默认捕获所有Exception:param default: 异常发生时返回的默认值(若re_raise为False):param re_raise: 捕获异常后是否重新抛出,默认False(不抛出):param error_msg: 自定义错误提示信息,默认为异常自带消息:return: 装饰后的函数"""def decorator(func: Callable) -> Callable:@functools.wraps(func) # 保留原函数元信息def wrapper(*args: Any, **kwargs: Any) -> Any:try:# 执行原函数return func(*args, **kwargs)except exceptions as e:# 若需要重新抛出异常,交给上层处理if re_raise:raise # 保持原异常类型和堆栈# 否则返回默认值return defaultreturn wrapperreturn decorator# 示例1:捕获ValueError和TypeError,返回默认值 @exception_handler(exceptions=(ValueError, TypeError),default=-1,error_msg="数值转换失败" ) def parse_number(s: str) -> int:"""将字符串转换为整数"""return int(s)# 示例2:捕获IOError,重新抛出异常(让调用者处理) @exception_handler(exceptions=(IOError,),re_raise=True,error_msg="文件读取失败" ) def read_file(path: str) -> str:"""读取文件内容"""with open(path, "r") as f:return f.read()
