Python装饰器用法详解

1. 引言

Python装饰器是一种强大而优雅的编程工具,它允许在不修改原函数代码的情况下,为函数添加额外的功能。装饰器本质上是一个可调用对象(通常是函数),它接受一个函数作为参数,并返回一个新的函数。这种设计模式广泛应用于日志记录、性能测试、事务处理、缓存、权限验证等场景。掌握装饰器是Python进阶编程的关键技能之一。

2. 理解装饰器的基础:函数也是对象

在深入装饰器之前,我们需要理解Python中函数的一个重要特性:函数是一等公民。这意味着函数可以像其他对象一样被传递、赋值给变量、作为参数传递给其他函数,以及从其他函数返回。

def greet(name):
    return f"Hello, {name}!"

# 函数作为对象赋值
welcome = greet
print(welcome("Alice"))  # 输出: Hello, Alice!

# 函数作为参数传递
def call_function(func, arg):
    return func(arg)

print(call_function(greet, "Bob"))  # 输出: Hello, Bob!

3. 装饰器的本质:闭包与嵌套函数

装饰器通常使用闭包来实现。闭包是一个函数对象,它记住了外部作用域中的变量。让我们通过一个简单的例子来理解装饰器的工作原理:

def simple_decorator(func):
    def wrapper():
        print("Something before the function")
        func()
        print("Something after the function")
    return wrapper

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

# 使用装饰器
say_hello = simple_decorator(say_hello)
say_hello()

输出:

Something before the function
Hello!
Something after the function

在这个例子中:

simple_decorator 是一个装饰器函数,它接受一个函数 func 作为参数

在装饰器内部,我们定义了一个新的函数 wrapper,它在调用原函数前后添加了额外的行为

装饰器返回这个 wrapper 函数

最后我们将 say_hello 变量指向返回的 wrapper 函数

4. @语法糖的使用

Python提供了 @ 语法糖来简化装饰器的使用,使代码更加简洁:

def simple_decorator(func):
    def wrapper():
        print("Before")
        func()
        print("After")
    return wrapper

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

say_hello()

输出:

Before
Hello!
After

@simple_decorator 等价于 say_hello = simple_decorator(say_hello)

5. 带参数的装饰器

有时我们需要创建可以接受参数的装饰器。这需要三层嵌套函数:最外层接收装饰器参数,中间层接收被装饰函数,最内层是实际执行的逻辑。

def repeat(num_times):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(num_times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

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

greet("Alice")

输出:

Hello, Alice!
Hello, Alice!
Hello, Alice!

在这个例子中:

repeat 是一个工厂函数,它接受 num_times 参数并返回一个装饰器

decorator 是实际的装饰器,它接收 func 函数

wrapper 是最终执行的函数,它调用 func 函数 num_times

6. 保留原函数的元信息

当我们使用装饰器时,原函数的元信息(如函数名、文档字符串等)会被 wrapper 函数覆盖。为了解决这个问题,我们可以使用 functools.wraps 装饰器:

from functools import wraps

def log_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper

@log_decorator
def add(a, b):
    """Add two numbers"""
    return a + b

print(add(3, 5))
print(add.__name__)
print(add.__doc__)

输出:

Calling add with args: (3, 5), kwargs: {}
add returned: 8
8
add
Add two numbers

functools.wraps 帮助保留了原函数的名称、文档字符串等元信息,这对于调试和文档生成非常重要。

7. 装饰器链(多个装饰器)

我们可以为一个函数应用多个装饰器,装饰器的应用顺序是从下到上(从靠近函数的装饰器开始):

def bold(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<b>{func(*args, **kwargs)}</b>"
    return wrapper

def italic(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f"<i>{func(*args, **kwargs)}</i>"
    return wrapper

@bold
@italic
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))  # 输出: <b><i>Hello, Alice!</i></b>

执行顺序相当于:greet = bold(italic(greet)),即先应用 italic 装饰器,再应用 bold 装饰器。

8. 类作为装饰器

除了函数,我们也可以使用类作为装饰器。类需要实现 __call__ 方法来使其可调用:

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count} of {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))
print(greet("Bob"))

输出:

Call 1 of greet
Hello, Alice!
Call 2 of greet
Hello, Bob!

类装饰器通常用于需要维护状态或复杂逻辑的场景。

9. 实际应用场景:计时装饰器

计时装饰器是装饰器的一个经典应用,用于测量函数的执行时间:

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.perf_counter()
        result = func(*args, **kwargs)
        end_time = time.perf_counter()
        run_time = end_time - start_time
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return result
    return wrapper

@timer
def sum_numbers(n):
    """Sum numbers from 1 to n"""
    return sum(range(1, n+1))

print(sum_numbers(1000000))

输出:

Finished 'sum_numbers' in 0.0432 secs
500000500000

10. 实际应用场景:缓存装饰器

缓存装饰器可以避免重复计算,显著提高递归或计算密集型函数的性能:

from functools import wraps

def cache(func):
    memo = {}

    @wraps(func)
    def wrapper(*args):
        if args not in memo:
            memo[args] = func(*args)
        return memo[args]
    return wrapper

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

print(fibonacci(35))  # 第一次计算需要时间
print(fibonacci(35))  # 第二次直接从缓存返回,非常快

11. 实际应用场景:权限验证装饰器

在Web开发中,装饰器常用于权限验证:

from functools import wraps

def permission_required(permission):
    def decorator(func):
        @wraps(func)
        def wrapper(user, *args, **kwargs):
            if permission not in user.permissions:
                raise PermissionError(f"User {user.name} lacks {permission} permission")
            return func(user, *args, **kwargs)
        return wrapper
    return decorator

class User:
    def __init__(self, name, permissions):
        self.name = name
        self.permissions = permissions

@permission_required("admin")
def delete_user(current_user, user_to_delete):
    print(f"User {current_user.name} deleted {user_to_delete.name}")

admin = User("Admin", ["admin", "editor"])
regular_user = User("Regular", ["editor"])

# 这将正常执行
delete_user(admin, regular_user)

# 这将抛出PermissionError
delete_user(regular_user, admin)

12. 装饰器参数验证

我们可以创建装饰器来验证函数参数:

from functools import wraps

def validate_types(**type_hints):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            # 将参数名与值对应起来
            bound_args = {}
            # 获取函数的参数名
            arg_names = func.__code__.co_varnames[:func.__code__.co_argcount]

            # 处理位置参数
            for i, arg in enumerate(args):
                if i < len(arg_names):
                    bound_args[arg_names[i]] = arg

            # 处理关键字参数
            bound_args.update(kwargs)

            # 验证类型
            for param_name, value in bound_args.items():
                if param_name in type_hints:
                    expected_type = type_hints[param_name]
                    if not isinstance(value, expected_type):
                        raise TypeError(
                            f"Argument '{param_name}' must be {expected_type}, "
                            f"got {type(value).__name__}"
                        )

            return func(*args, **kwargs)
        return wrapper
    return decorator

@validate_types(name=str, age=int, hobbies=list)
def create_user(name, age, hobbies=[]):
    return f"User {name} ({age}) with hobbies: {', '.join(hobbies)}"

print(create_user("Alice", 30, ["reading", "coding"]))  # 正常
# create_user("Bob", "thirty", [])  # 这会抛出TypeError: Argument 'age' must be <class 'int'>, got str

13. 装饰器的陷阱与注意事项

装饰器执行时机:装饰器在模块导入时立即执行,而不是在函数调用时。这意味着装饰器中的任何副作用都会在导入时发生。

装饰后的函数身份:装饰器会替换原函数,如果代码依赖函数的 __name__ 或其他属性,应使用 functools.wraps 保留这些属性。

装饰器顺序:多个装饰器时,顺序很重要。@decorator1 @decorator2 func 等价于 func = decorator1(decorator2(func))

带参数装饰器:带参数的装饰器需要额外的函数层来接收参数。

类方法装饰:装饰类方法时,self 参数会被自动传递,但装饰器本身需要正确处理。

from functools import wraps

def method_decorator(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        print(f"Calling {func.__name__} on instance {self}")
        return func(self, *args, **kwargs)
    return wrapper

class MyClass:
    @method_decorator
    def my_method(self, value):
        return value * 2

obj = MyClass()
print(obj.my_method(5))  # 输出: Calling my_method on instance <__main__.MyClass object> 10

14. 高级装饰器技术

14.1. 装饰器类与状态保持

当装饰器需要保持状态时,使用类作为装饰器是更好的选择:

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call {self.count} of {self.func.__name__}")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}!"

print(greet("Alice"))
print(greet("Bob"))
print(greet.count)  # 访问装饰器状态

14.2. 可选参数的装饰器

创建可以带参数或不带参数的装饰器:

from functools import wraps

def optional_debug(func=None, *, prefix="DEBUG"):
    if func is None:
        # 被调用时带参数,返回装饰器
        return lambda f: optional_debug(f, prefix=prefix)

    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"{prefix}: {func.__name__} called")
        return func(*args, **kwargs)
    return wrapper

# 不带参数使用
@optional_debug
def add(a, b):
    return a + b

# 带参数使用
@optional_debug(prefix="INFO")
def multiply(a, b):
    return a * b

print(add(2, 3))
print(multiply(2, 3))

15. 总结

Python装饰器是一种强大而灵活的代码增强机制,它允许我们以非侵入式的方式为函数添加额外功能。通过深入理解装饰器的工作原理——函数作为对象、闭包和嵌套函数——我们可以创建各种实用的装饰器来解决实际问题。

关键要点包括:

装饰器本质上是一个接收函数并返回新函数的可调用对象

使用 @ 语法糖可以简化装饰器的应用

带参数的装饰器需要三层嵌套函数结构

使用 functools.wraps 可以保留原函数的元信息

装饰器可以叠加使用,顺序从下到上

类装饰器适合需要保持状态的场景

实际应用包括日志记录、性能测试、缓存、权限验证等

掌握装饰器不仅能帮助我们编写更简洁、可维护的代码,还能让我们更好地理解Python的高级特性。随着经验的积累,你会发现装饰器是解决许多编程难题的优雅方案。

发表回复

后才能评论