在 Python 中,装饰器(Decorator) 是一种强大的语法工具,用于在不修改原函数 / 类代码的前提下,为其添加额外功能(如日志记录、性能计时、权限校验、缓存等)。它本质上是一个接收函数 / 类作为参数,并返回新函数 / 类的高阶函数,通过 @ 语法糖简化使用,极大提升了代码的复用性和可读性。

一、装饰器的核心本质

装饰器的核心逻辑可以概括为:“包裹原函数,在不改变其核心逻辑的情况下,在执行前后插入额外操作”

从技术角度看,装饰器是一个满足以下条件的函数:

  1. 接收一个函数(或类)作为参数;
  2. 内部定义一个 “包装函数(wrapper)”,用于包裹原函数并添加额外逻辑;
  3. 返回这个包装函数(替代原函数)。

二、基础装饰器:无参数装饰器

先从最简单的 “无参数装饰器” 入手,理解其基本工作原理。

1. 定义与使用

假设我们需要为函数添加 “执行前后打印日志” 的功能,用装饰器实现如下:

# 定义装饰器:为函数添加日志功能
def log_decorator(func):
    # 定义包装函数(wrapper),用于包裹原函数
    def wrapper():
        print(f"[日志] 开始执行函数:{func.__name__}")  # 执行前的额外操作
        result = func()  # 调用原函数
        print(f"[日志] 函数 {func.__name__} 执行结束")  # 执行后的额外操作
        return result  # 返回原函数的结果
    return wrapper  # 返回包装函数

# 使用装饰器:用 @ 语法糖将装饰器应用到目标函数
@log_decorator
def say_hello():
    print("Hello, 装饰器!")

# 调用被装饰后的函数
say_hello()

输出结果

[日志] 开始执行函数:say_hello
Hello, 装饰器!
[日志] 函数 say_hello 执行结束

2. 执行流程解析

@log_decorator 是语法糖,等价于:say_hello = log_decorator(say_hello)

整个过程分为三步:

  1. 定义 say_hello 函数时,@log_decorator 触发 log_decorator(say_hello) 调用;
  2. log_decorator 接收 say_hello 作为参数,定义 wrapper 函数并返回;
  3. 原 say_hello 被重新赋值为 wrapper 函数,因此调用 say_hello() 实际执行的是 wrapper()

3. 适配带参数的函数

上面的装饰器只能装饰无参数函数,若目标函数有参数(如 def add(a, b): ...),则需要让 wrapper 支持参数传递。通过 *args 和 **kwargs 可让 wrapper 接收任意数量的位置参数和关键字参数,实现通用装饰器:

def log_decorator(func):
    # wrapper 用 *args 和 **kwargs 接收任意参数
    def wrapper(*args, **kwargs):
        print(f"[日志] 开始执行 {func.__name__},参数:{args}, {kwargs}")
        result = func(*args, **kwargs)  # 传递参数给原函数
        print(f"[日志] {func.__name__} 执行结束,结果:{result}")
        return result
    return wrapper

@log_decorator
def add(a, b):
    return a + b

@log_decorator
def greet(name, prefix="Hello"):
    return f"{prefix}, {name}!"

# 调用测试
print(add(2, 3))  # 带位置参数
print(greet("Alice", prefix="Hi"))  # 带位置+关键字参数

输出结果

[日志] 开始执行 add,参数:(2, 3), {}
[日志] add 执行结束,结果:5
5
[日志] 开始执行 greet,参数:('Alice',), {'prefix': 'Hi'}
[日志] greet 执行结束,结果:Hi, Alice!
Hi, Alice!

通过 *args 和 **kwargs,装饰器可适配任意参数的函数,通用性极大提升。

三、带参数的装饰器

有时需要让装饰器 “可配置”(如指定日志级别、缓存过期时间等),此时需定义带参数的装饰器。其核心是在基础装饰器外层再嵌套一层函数,用于接收装饰器参数。

1. 定义与使用

例如,实现一个可指定日志级别的装饰器:

# 带参数的装饰器:外层函数接收装饰器参数
def log_decorator(level="INFO"):
    # 中间层函数接收目标函数
    def decorator(func):
        # 内层 wrapper 函数包裹原函数
        def wrapper(*args, **kwargs):
            print(f"[{level}] 开始执行 {func.__name__}")  # 使用装饰器参数
            result = func(*args, **kwargs)
            print(f"[{level}] {func.__name__} 执行结束")
            return result
        return wrapper
    return decorator

# 使用带参数的装饰器:指定 level 为 "DEBUG"
@log_decorator(level="DEBUG")
def multiply(a, b):
    return a * b

# 不指定参数,使用默认值 "INFO"
@log_decorator()
def subtract(a, b):
    return a - b

# 调用测试
print(multiply(3, 4))
print(subtract(10, 5))

输出结果

[DEBUG] 开始执行 multiply
[DEBUG] multiply 执行结束
12
[INFO] 开始执行 subtract
[INFO] subtract 执行结束
5

2. 执行流程解析

@log_decorator(level="DEBUG") 等价于:multiply = log_decorator(level="DEBUG")(multiply)

过程分为四步:

  1. 调用 log_decorator(level="DEBUG"),返回中间层函数 decorator
  2. 调用 decorator(multiply),接收目标函数 multiply
  3. 定义 wrapper 并返回;
  4. 原 multiply 被赋值为 wrapper,调用时执行 wrapper

四、保留原函数的元信息

装饰器会默认 “覆盖” 原函数的元信息(如 __name____doc__ 等),导致调试困难。例如:

def decorator(func):
    def wrapper():
        """wrapper 函数的文档字符串"""
        func()
    return wrapper

@decorator
def original_func():
    """original_func 函数的文档字符串"""
    print("原函数")

print(original_func.__name__)  # 输出:wrapper(被覆盖)
print(original_func.__doc__)   # 输出:wrapper 函数的文档字符串(被覆盖)

解决方法:使用 functools.wraps 装饰 wrapper,保留原函数的元信息:

import functools

def decorator(func):
    # 用 functools.wraps 保留原函数元信息
    @functools.wraps(func)
    def wrapper():
        """wrapper 函数的文档字符串"""
        func()
    return wrapper

@decorator
def original_func():
    """original_func 函数的文档字符串"""
    print("原函数")

print(original_func.__name__)  # 输出:original_func(正确保留)
print(original_func.__doc__)   # 输出:original_func 函数的文档字符串(正确保留)

functools.wraps 是装饰器开发的最佳实践,必须添加以确保元信息正确。

五、多个装饰器的叠加使用

一个函数可以同时应用多个装饰器,执行顺序为从下到上包裹(即靠近函数的装饰器先执行)。

import functools

# 装饰器1:打印执行时间
def timer_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        import time
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"[计时] {func.__name__} 执行耗时:{end - start:.4f}秒")
        return result
    return wrapper

# 装饰器2:打印日志
def log_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"[日志] 开始执行 {func.__name__}")
        result = func(*args, **kwargs)
        print(f"[日志] {func.__name__} 执行结束")
        return result
    return wrapper

# 叠加使用装饰器:先执行 log_decorator,再执行 timer_decorator
@timer_decorator
@log_decorator
def slow_func(seconds):
    import time
    time.sleep(seconds)  # 模拟耗时操作
    return "完成"

slow_func(1)

输出结果

[日志] 开始执行 slow_func  # log_decorator 的前置操作
[日志] slow_func 执行结束  # log_decorator 的后置操作
[计时] slow_func 执行耗时:1.0012秒  # timer_decorator 的后置操作

执行顺序解析@timer_decorator 和 @log_decorator 等价于:slow_func = timer_decorator(log_decorator(slow_func))因此,调用时的顺序是:timer_decorator 的 wrapper → log_decorator 的 wrapper → 原 slow_func

六、类装饰器

除了装饰函数,装饰器也可以装饰类(通过接收类作为参数,返回新类)。类装饰器常用于动态修改类的属性或方法

1. 基础类装饰器

def add_attr_decorator(cls):
    # 为类添加新属性
    cls.version = "1.0"
    # 为类添加新方法
    def new_method(self):
        return f"我是 {self.__class__.__name__} 的新方法"
    cls.new_method = new_method
    return cls

@add_attr_decorator
class MyClass:
    def __init__(self, name):
        self.name = name

# 测试被装饰的类
obj = MyClass("测试")
print(obj.version)  # 输出:1.0(新增的属性)
print(obj.new_method())  # 输出:我是 MyClass 的新方法(新增的方法)

2. 带参数的类装饰器

类似函数装饰器,类装饰器也可带参数(外层函数接收参数,内层函数处理类):

def add_prefix_decorator(prefix):
    def decorator(cls):
        # 为类的方法添加前缀
        class WrappedClass(cls):
            def greet(self):
                return f"{prefix}: {super().greet()}"  # 调用父类方法
        return WrappedClass
    return decorator

@add_prefix_decorator(prefix="【提示】")
class Greeting:
    def greet(self):
        return "Hello"

obj = Greeting()
print(obj.greet())  # 输出:【提示】: Hello

七、装饰器的典型应用场景

装饰器在实际开发中应用广泛,以下是常见场景:

1. 日志记录

为函数添加输入参数、返回值、执行时间等日志,便于调试和监控:

import functools
import logging

logging.basicConfig(level=logging.INFO)

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        logging.info(f"调用 {func.__name__},参数:{args}, {kwargs}")
        result = func(*args, **kwargs)
        logging.info(f"{func.__name__} 返回:{result}")
        return result
    return wrapper

@log
def calculate(a, b):
    return a + b

2. 性能计时

统计函数执行时间,用于性能分析:

import functools
import time

def timer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} 耗时:{end - start:.6f}秒")
        return result
    return wrapper

@timer
def big_loop(n):
    for i in range(n):
        pass

3. 权限验证

在接口调用前验证用户权限,如登录状态、角色权限等:

import functools

def require_login(func):
    @functools.wraps(func)
    def wrapper(user, *args, **kwargs):
        if not user.is_login:
            raise PermissionError("请先登录")
        return func(user, *args, **kwargs)
    return wrapper

@require_login
def view_profile(user):
    return f"用户 {user.name} 的个人资料"

4. 缓存(记忆化)

缓存函数的计算结果,避免重复计算(适用于耗时且输入固定的函数):

import functools

def cache(func):
    cache_dict = {}  # 缓存字典:key=参数,value=结果

    @functools.wraps(func)
    def wrapper(*args):
        if args not in cache_dict:
            cache_dict[args] = func(*args)  # 计算并缓存结果
            print(f"缓存 {args} 的结果")
        else:
            print(f"使用 {args} 的缓存结果")
        return cache_dict[args]
    return wrapper

@cache
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

fib(5)  # 首次计算,缓存中间结果
fib(5)  # 直接使用缓存

八、装饰器的最佳实践

  1. 始终使用 functools.wraps:保留原函数的元信息(__name____doc__ 等),避免调试混乱。
  2. 单一职责:一个装饰器只做一件事(如日志、计时、权限验证分开实现),提高复用性。
  3. 避免过度装饰:过多装饰器会增加函数调用栈复杂度,降低可读性。
  4. 文档说明:为装饰器添加文档字符串,说明其功能和参数(尤其是带参数的装饰器)。
  5. 处理异常:若装饰器可能触发异常(如权限验证失败),需明确抛出并说明。

总结

装饰器是 Python 中极具特色的语法,其核心是通过 “包装函数” 在不修改原代码的情况下扩展功能。从基础的无参数装饰器,到带参数的装饰器、类装饰器,再到多装饰器叠加,掌握这些用法能极大提升代码的模块化和复用性。