跳转至

装饰器 (Decorators)

装饰器是 Python 中一个非常强大且常用的功能,它允许我们在不修改原函数代码的情况下,动态地增加函数的功能。本质上,装饰器是一个接收函数作为参数并返回一个新函数的高阶函数。

1. 什么是装饰器?

假设我们有一个函数,我们想在它执行前后打印日志,但不想修改函数本身的逻辑。

不使用装饰器

def my_func():
    print("Executing my_func")

def log_wrapper(func):
    print("Before calling...")
    func()
    print("After calling...")

log_wrapper(my_func)

使用装饰器

Python 提供了 @ 语法糖,使上面的逻辑更加简洁。

def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")
    return wrapper

@my_decorator
def say_hello():
    print("Hello!")

say_hello()
# 输出:
# Something is happening before the function is called.
# Hello!
# Something is happening after the function is called.

2. 装饰带参数的函数

如果你要装饰的函数接收参数,那么装饰器内部的 wrapper 函数也必须能够接收这些参数。最通用的做法是使用 *args**kwargs

def log_args(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function with args: {args}, kwargs: {kwargs}")
        return func(*args, **kwargs) # 记得返回原函数的执行结果
    return wrapper

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

result = add(3, 5)
# 输出: Calling function with args: (3, 5), kwargs: {}
print(result) # 输出: 8

关于 *args**kwargs

这两个是 Python 中的可变参数语法,在装饰器中非常关键:

  • *args (Arguments):用于接收任意数量的位置参数(Positional Arguments),并将它们打包成一个元组 (Tuple)。例如,调用 add(1, 2) 时,args 就是 (1, 2)
  • **kwargs (Keyword Arguments):用于接收任意数量的关键字参数(Keyword Arguments),并将它们打包成一个字典 (Dictionary)。例如,调用 func(a=1, b=2) 时,kwargs 就是 {'a': 1, 'b': 2}

wrapper 函数中使用它们,可以确保无论原函数 func 定义了什么样的参数,wrapper 都能正确地接收并传递给它。

3. 带参数的装饰器

有时候,我们需要给装饰器本身传递参数,比如设置日志级别或重试次数。这时候需要多包一层函数。

def repeat(num_times):
    def decorator_repeat(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result # 放在循环外,只返回最后一次调用的结果
        return wrapper
    return decorator_repeat

@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")

greet("World")
# 输出:
# Hello World
# Hello World
# Hello World

如果原函数没有返回值怎么办?

这也是完全没问题的。在 Python 中,如果一个函数没有显式的 return 语句,它默认返回 None

所以在上面的例子中,result 会接收到 None,并且 wrapper 最终也会返回 None。这样做的好处是它可以保持原函数的行为一致:如果原函数有返回值,装饰器就返回它;如果没有,就返回 None,就像原函数一样。

为什么需要三层函数?

这确实容易让人头晕,我们可以这样拆解:

  1. 第一层 repeat(num_times):这是最外层,它的任务是接收你传递给装饰器的参数(如 3),并返回真正的装饰器函数(即第二层)。
  2. 第二层 decorator_repeat(func):这是真正的装饰器,它的任务是接收被装饰的函数(如 greet),并返回包装后的函数(即第三层)。
  3. 第三层 wrapper(*args, **kwargs):这是最终执行的函数,它的任务是接收原函数的参数(如 "World"),并在其中执行具体的逻辑(由于闭包特性,它可以同时访问 num_timesfunc)。

简单来说:第一层收装饰器参数,第二层收函数,第三层收函数的参数。

4. functools.wraps 的重要性

使用装饰器后,原函数的元信息(如 __name____doc__)会被 wrapper 函数覆盖。为了保留原函数的元信息,应该使用 functools.wraps 装饰 wrapper 函数。

import functools

def my_decorator(func):
    @functools.wraps(func) # 保留原函数的元数据
    def wrapper(*args, **kwargs):
        """Wrapper function docstring"""
        print("Calling decorated function")
        return func(*args, **kwargs)
    return wrapper

@my_decorator
def example():
    """Example function docstring"""
    print("Called example function")

print(example.__name__) # 输出: example (如果没有 @wraps,会输出 wrapper)
print(example.__doc__)  # 输出: Example function docstring

5. 类装饰器

装饰器不仅可以修饰函数,也可以修饰类。

def singleton(cls): # cls 这里接收的是被装饰的类本身
    """单例模式装饰器"""
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs) # 相当于调用 DatabaseConnection()
        return instances[cls]
    return get_instance

@singleton
class DatabaseConnection:
    def __init__(self):
        print("Database connected")

db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(db1 is db2) # 输出: True

总结

  • 装饰器是修改函数行为的一种优雅方式,遵循“开放封闭原则”。
  • 使用 @decorator 语法糖应用装饰器。
  • 使用 *args**kwargs 处理参数。
  • 使用 functools.wraps 保留原函数元数据。
  • 常见应用:日志记录、性能测试、权限校验、缓存(@lru_cache)。

下一步

掌握了这些高级特性后,我们可以将目光转向更实战的领域:算法与数据结构。在刷 LeetCode 时,Python 提供了许多内置的高效数据结构,能让你的解题事半功倍。

👉 前往下一章:数据结构 (Data Structures)