Python 装饰器

Python 装饰器(Decorator)是一种高级语法,用于在不修改原函数代码的前提下,动态扩展函数或类的功能。它本质上是一个接收函数 / 类作为参数,并返回新函数 / 类的可调用对象,是函数式编程的典型应用。

1、核心原理

装饰器的核心依赖于 Python 的两个特性:

  1. 函数:函数可以作为参数传递、作为返回值返回、赋值给变量。
  2. 闭包:嵌套函数可以访问外层函数的变量和参数(即使外层函数已执行完毕)。

1.1 装饰器的基本结构

一个最简单的装饰器由外层函数(接收原函数)内层函数(包装原函数) 组成,最终返回内层函数:

python 复制代码
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 装饰器的执行时机

装饰器在模块加载时(导入时) 就会执行,而非函数调用时。例如:

python 复制代码
print("模块加载中...")

def decorator(func):
    print(f"装饰器执行:装饰 {func.__name__}")
    return func

@decorator
def func1():
    pass

@decorator
def func2():
    pass

print("模块加载完成")

执行结果(先执行装饰器,再执行后续代码):

复制代码
模块加载中...
装饰器执行:装饰 func1
装饰器执行:装饰 func2
模块加载完成

2、进阶用法

2.1 带参数的装饰器

若需要给装饰器传递参数(如日志的级别、缓存的过期时间),需在外层再套一层 "参数接收函数":

python 复制代码
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 result
        return wrapper
    return decorator

# 使用带参数的装饰器
@log_decorator(level="INFO")
def add(a, b):
    return a + b

print(add(1, 2))

执行结果:

复制代码
[INFO] 函数 add 开始执行
[INFO] 函数 add 执行结束
3

2.2 类装饰器

类也可以作为装饰器,核心是实现 __call__ 方法(使类实例可调用):

python 复制代码
class TimerDecorator:
    def __init__(self, func):  # 初始化时接收原函数
        self.func = func

    def __call__(self, *args, **kwargs):  # 调用时执行装饰逻辑
        import time
        start = 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 time
    time.sleep(seconds)  # 模拟耗时操作

slow_func(1)  # 执行被装饰的函数

执行结果:

复制代码
函数 slow_func 执行耗时:1.00s

2.3 装饰器的嵌套

多个装饰器可以叠加使用,嵌套装饰器的核心是装饰顺序调用顺序的区别,理解这两个顺序是掌握嵌套装饰器的关键。

python 复制代码
# 定义两个装饰器
def decorator_a(func):
    def wrapper(*args, **kwargs):
        print("装饰器 A 开始")
        result = func(*args, **kwargs)
        print("装饰器 A 结束")
        return result
    return wrapper

def decorator_b(func):
    def wrapper(*args, **kwargs):
        print("装饰器 B 开始")
        result = func(*args, **kwargs)
        print("装饰器 B 结束")
        return result
    return wrapper

# 嵌套应用装饰器
@decorator_a
@decorator_b
def target():
    print("执行目标函数")

# 调用目标函数
target()

执行结果:

复制代码
装饰器 A 开始
装饰器 B 开始
执行目标函数
装饰器 B 结束
装饰器 A 结束
2.3.1 装饰顺序:从下到上("包裹顺序")

装饰器的应用过程发生在函数定义时,本质是对函数的 "层层包裹"。对于上面的例子:

python 复制代码
@decorator_a
@decorator_b
def target(): ...

等价于手动执行:

python 复制代码
# 第一步:用 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() 调用为例,执行流程拆解:

  1. 调用 target() 实际是调用 decorator_a 返回的 wrapper_a
  2. wrapper_a 中先执行自身逻辑(print("装饰器 A 开始")),然后调用 func(此时 funcdecorator_b 返回的 wrapper_b);
  3. wrapper_b 中先执行自身逻辑(print("装饰器 B 开始")),然后调用 func(此时 func 是原始 target 函数);
  4. 执行原始 target 函数(print("执行目标函数"));
  5. 原始函数执行完毕,返回结果给 wrapper_bwrapper_b 执行后续逻辑(print("装饰器 B 结束"));
  6. wrapper_b 执行完毕,返回结果给 wrapper_awrapper_a 执行后续逻辑(print("装饰器 A 结束"));
  7. 最终结果返回给调用者。

简言之:调用时先执行外层装饰器的 "前置逻辑",再执行内层装饰器的 "前置逻辑",然后是原始函数,最后按相反顺序执行各装饰器的 "后置逻辑"

2.4 保留原函数元信息

装饰器默认会覆盖原函数的元信息(如 __name____doc__),需使用 functools.wraps 修复:

python 复制代码
import functools

def 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 的文档字符串"""
    pass

print(f1.__name__)  # 输出:wrapper(错误)
print(f1.__doc__)   # 输出:None(错误)
print(f2.__name__)  # 输出:f2(正确)
print(f2.__doc__)   # 输出:f2 的文档字符串(正确)

2.5 装饰类的方法

2.5.1 装饰实例方法

实例方法是类中最常见的方法,第一个参数默认是 self(指向实例本身)。装饰实例方法时,装饰器需要正确传递 self 及其他参数。

用装饰器记录实例方法的调用日志

python 复制代码
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 result
    return wrapper

# 定义类,用装饰器装饰实例方法
class User:
    def __init__(self, name):
        self.name = name  # 实例属性

    # 用装饰器装饰实例方法
    @log_instance_method
    def 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 参数的传递。

用装饰器验证类方法的参数

python 复制代码
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_positive
    def 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 装饰,无默认参数(与普通函数类似)。装饰静态方法时,处理逻辑与装饰普通函数一致。

用装饰器记录静态方法的执行时间

python 复制代码
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 result
    return wrapper

# 定义类,用装饰器装饰静态方法
class StringUtils:
    # 装饰器顺序:@staticmethod 在下,功能装饰器在上
    @staticmethod
    @timer
    def reverse(s):
        """反转字符串(静态方法)"""
        time.sleep(0.1)  # 模拟耗时操作
        return s[::-1]

# 测试
print(StringUtils.reverse("hello"))  # 输出:olleh

执行结果:

复制代码
[计时] 静态方法 reverse 执行耗时:0.1002 秒
olleh

关键说明:

  • 静态方法无 selfcls,装饰器的 wrapper 函数直接用 *args, **kwargs 接收参数即可;
  • 装饰器顺序与类方法类似:@staticmethod 在下,功能装饰器在上,确保装饰的是静态方法。

3、装饰器的应用场景

  1. 日志记录:自动记录函数的调用参数、返回值、执行时间。

  2. 权限验证:调用函数前检查用户权限(如登录状态)。

    python 复制代码
    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} 元")
  3. 缓存结果:缓存函数的计算结果,避免重复计算。

    python 复制代码
    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)
  4. 异常处理:统一捕获函数执行中的异常,避免程序崩溃。

    python 复制代码
    import functools
    from typing import Callable, Any, Type, Tuple, Optional
    
    def 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 default
            return wrapper
        return 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()
相关推荐
快秃头的码农5 小时前
vscode搭建python项目隔离的虚拟环境
ide·vscode·python
mxpan5 小时前
从 0 到 1:用 Python 对接阿里云 DashScope,轻松实现 AI 对话功能
python·ai编程
被放养的研究生6 小时前
dir()与help()
python
我的xiaodoujiao6 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 20--PO(POM) 设计模式和用例撰写
python·学习·测试工具·设计模式·pytest
子不语1806 小时前
深度学习(一)——基础知识:Python语言/解释器/环境/编辑器
python·学习
爱学习的爬虫7 小时前
Python实现Jenkins实现自动化执行Job
python·自动化·jenkins
一晌小贪欢7 小时前
Python爬虫第9课:验证码识别与自动化处理
爬虫·python·自动化·网络爬虫·python爬虫·python3
bin91537 小时前
AI工具赋能Python开发者:项目开发中的创意守护与效率革命
开发语言·人工智能·python·工具·ai工具
被放养的研究生7 小时前
Python常用的一些语句
开发语言·python