MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

理解Python中的装饰器

2021-12-116.6k 阅读

什么是装饰器

在Python中,装饰器是一种强大而灵活的工具,它允许你在不修改现有函数或类代码的情况下,为它们添加额外的功能。装饰器本质上是一个函数,它以另一个函数作为输入参数,并返回一个新的函数。这个新函数通常包含了原始函数的功能,同时还添加了额外的功能。

从语法上看,装饰器使用 @ 符号,紧跟在装饰器函数名之后,放在被装饰的函数或类的定义之前。例如:

def my_decorator(func):
    def wrapper():
        print("在函数执行前做一些事情")
        func()
        print("在函数执行后做一些事情")
    return wrapper

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

say_hello()

在上述代码中,my_decorator 是一个装饰器函数,它接受一个函数 func 作为参数,并返回一个内部定义的 wrapper 函数。@my_decorator 这行代码将 say_hello 函数传递给 my_decorator 装饰器,然后 say_hello 函数实际上被替换为 my_decorator 返回的 wrapper 函数。当调用 say_hello() 时,实际上执行的是 wrapper 函数,它会先打印 "在函数执行前做一些事情",然后调用原始的 say_hello 函数,最后打印 "在函数执行后做一些事情"。

装饰器的工作原理

函数即对象

要理解装饰器,首先要明白在Python中函数是一等公民,这意味着函数可以像其他数据类型(如整数、字符串、列表等)一样被赋值给变量、作为参数传递给其他函数、作为函数的返回值等。例如:

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

# 将函数赋值给变量
func = add
result = func(3, 5)
print(result)  

在上述代码中,add 函数被赋值给变量 func,然后通过 func 变量调用了 add 函数,就像直接调用 add 函数一样。

嵌套函数

装饰器通常使用嵌套函数来实现。嵌套函数是指在一个函数内部定义另一个函数。例如:

def outer():
    def inner():
        print("这是内部函数")
    inner()

outer()  

outer 函数内部定义了 inner 函数,并且在 outer 函数内部调用了 inner 函数。注意,inner 函数在 outer 函数外部是不可见的,除非 outer 函数返回 inner 函数。

闭包

闭包是装饰器实现的关键概念之一。闭包是指一个函数对象,它记住了定义时的环境变量,即使在那个环境已经不存在时,仍然可以访问那些变量。例如:

def outer(x):
    def inner():
        print(f"x的值是: {x}")
    return inner

closure = outer(10)
closure()  

在上述代码中,outer 函数接受一个参数 x,并返回内部定义的 inner 函数。当 outer 函数返回 inner 函数时,inner 函数形成了一个闭包,它记住了 x 的值为 10。即使 outer 函数的调用已经结束,x 变量在 outer 函数的局部作用域中已经不存在,但 closure 函数仍然可以访问并打印 x 的值。

装饰器的实现

回到装饰器的例子,我们再详细分析一下:

def my_decorator(func):
    def wrapper():
        print("在函数执行前做一些事情")
        func()
        print("在函数执行后做一些事情")
    return wrapper

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

say_hello()
  1. 首先定义了 my_decorator 装饰器函数,它接受一个函数 func 作为参数。
  2. my_decorator 内部定义了 wrapper 函数,wrapper 函数可以访问到 my_decorator 的参数 func,形成了闭包。
  3. wrapper 函数在执行原始函数 func 前后添加了额外的打印语句,实现了功能的增强。
  4. my_decorator 函数返回 wrapper 函数。
  5. 当使用 @my_decorator 装饰 say_hello 函数时,实际上是将 say_hello 函数作为参数传递给 my_decorator 函数,并将 my_decorator 函数返回的 wrapper 函数重新赋值给 say_hello 变量。所以当调用 say_hello() 时,实际执行的是 wrapper 函数。

装饰器的应用场景

日志记录

在开发中,经常需要记录函数的执行情况,例如函数的输入参数、执行时间、返回值等。装饰器可以方便地实现这一功能,而无需在每个需要记录日志的函数内部编写大量重复的日志记录代码。

import logging

def log_function_call(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_function_call
def add_numbers(a, b):
    return a + b

add_numbers(3, 5)

在上述代码中,log_function_call 装饰器记录了被装饰函数的调用信息和返回值。*args**kwargs 用于处理任意数量和类型的参数,确保装饰器可以应用于各种函数。

性能测量

测量函数的执行时间对于优化代码性能非常重要。装饰器可以很容易地实现这一功能。

import time

def measure_performance(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"函数 {func.__name__} 执行时间: {end_time - start_time} 秒")
        return result
    return wrapper

@measure_performance
def long_running_function():
    time.sleep(2)
    return "完成"

long_running_function()

在上述代码中,measure_performance 装饰器通过记录函数开始和结束执行的时间,计算并打印出函数的执行时间。

身份验证和授权

在Web开发等场景中,需要对用户进行身份验证和授权,确保只有有权限的用户才能访问某些功能。装饰器可以用于实现这一逻辑。

def require_authentication(func):
    def wrapper(*args, **kwargs):
        is_authenticated = True  
        if is_authenticated:
            return func(*args, **kwargs)
        else:
            print("未授权访问")
    return wrapper

@require_authentication
def sensitive_operation():
    print("执行敏感操作")

sensitive_operation()

在上述代码中,require_authentication 装饰器模拟了身份验证逻辑,如果用户已认证(这里简单设置为 True),则执行被装饰的函数,否则提示未授权访问。

缓存

对于一些计算成本较高的函数,为了提高性能,可以缓存函数的结果,避免重复计算。装饰器可以实现缓存功能。

def cache_result(func):
    cache = {}
    def wrapper(*args, **kwargs):
        key = (args, tuple(sorted(kwargs.items())))
        if key in cache:
            return cache[key]
        result = func(*args, **kwargs)
        cache[key] = result
        return result
    return wrapper

@cache_result
def expensive_calculation(a, b):
    print("执行昂贵的计算")
    time.sleep(2)
    return a * b

print(expensive_calculation(3, 5))
print(expensive_calculation(3, 5))  

在上述代码中,cache_result 装饰器使用一个字典 cache 来存储函数的计算结果。当函数被调用时,它根据输入参数生成一个唯一的键,如果键存在于缓存中,则直接返回缓存的结果,否则执行函数并将结果存入缓存。

带参数的装饰器

前面介绍的装饰器都是不带参数的简单装饰器。有时候,我们希望装饰器能够接受参数,以便更灵活地定制装饰器的行为。带参数的装饰器实际上是一个返回装饰器函数的函数。

基本原理

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

@repeat(3)
def say_hello():
    print("Hello!")

say_hello()

在上述代码中,repeat 是一个接受参数 n 的函数,它返回一个装饰器函数 decoratordecorator 函数接受被装饰的函数 func 作为参数,并返回一个 wrapper 函数。wrapper 函数会根据 n 的值重复调用原始函数 func

应用场景

带参数的装饰器在很多场景下非常有用,比如在日志记录装饰器中,可以根据不同的需求设置日志的级别。

import logging

def set_log_level(level):
    def decorator(func):
        def wrapper(*args, **kwargs):
            logger = logging.getLogger(__name__)
            logger.setLevel(level)
            result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@set_log_level(logging.DEBUG)
def complex_operation():
    logging.debug("执行复杂操作")
    # 复杂操作的代码

complex_operation()

在上述代码中,set_log_level 装饰器接受一个日志级别参数 level,并在装饰的函数执行前设置相应的日志级别。

类装饰器

除了函数装饰器,Python还支持类装饰器。类装饰器本质上是一个类,它实现了 __call__ 方法,使得类的实例可以像函数一样被调用。

类装饰器的基本实现

class MyDecorator:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        print("在函数执行前做一些事情")
        result = self.func(*args, **kwargs)
        print("在函数执行后做一些事情")
        return result

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

say_hello()

在上述代码中,MyDecorator 类接受被装饰的函数 func 作为构造函数的参数,并在 __call__ 方法中实现了装饰器的逻辑,即在调用原始函数前后打印一些信息。

类装饰器的应用场景

类装饰器在一些需要更复杂状态管理或面向对象编程风格的场景中非常有用。例如,一个类装饰器可以用于统计一个函数被调用的次数,并将这个统计信息作为类的属性保存。

class CallCounter:
    def __init__(self, func):
        self.func = func
        self.call_count = 0

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"函数 {self.func.__name__} 被调用了 {self.call_count} 次")
        return self.func(*args, **kwargs)

@CallCounter
def simple_function():
    print("执行简单函数")

simple_function()
simple_function()

在上述代码中,CallCounter 类装饰器统计了 simple_function 函数被调用的次数,并在每次调用时打印调用次数。

装饰器的顺序

当一个函数被多个装饰器装饰时,装饰器的顺序非常重要。装饰器的应用顺序是从最靠近函数定义的装饰器开始,向外依次应用。

示例代码

def decorator1(func):
    def wrapper1(*args, **kwargs):
        print("装饰器1在函数执行前")
        result = func(*args, **kwargs)
        print("装饰器1在函数执行后")
        return result
    return wrapper1

def decorator2(func):
    def wrapper2(*args, **kwargs):
        print("装饰器2在函数执行前")
        result = func(*args, **kwargs)
        print("装饰器2在函数执行后")
        return result
    return wrapper2

@decorator1
@decorator2
def say_hello():
    print("Hello!")

say_hello()

在上述代码中,say_hello 函数先被 decorator2 装饰,然后被 decorator1 装饰。执行结果如下:

装饰器1在函数执行前
装饰器2在函数执行前
Hello!
装饰器2在函数执行后
装饰器1在函数执行后

可以看到,先执行 decorator1 的外层逻辑,然后执行 decorator2 的外层逻辑,接着执行原始函数,再依次执行 decorator2decorator1 的内层逻辑。

装饰器与函数元数据

当使用装饰器装饰函数时,会出现一个问题,即被装饰函数的元数据(如函数名、文档字符串等)会被替换为装饰器内部 wrapper 函数的元数据。这可能会给调试和代码理解带来一些不便。

示例

def my_decorator(func):
    def wrapper():
        """这是wrapper函数的文档字符串"""
        print("在函数执行前做一些事情")
        func()
        print("在函数执行后做一些事情")
    return wrapper

@my_decorator
def say_hello():
    """这是say_hello函数的文档字符串"""
    print("Hello!")

print(say_hello.__name__)  
print(say_hello.__doc__)  

在上述代码中,say_hello 函数被装饰后,其 __name__ 变成了 wrapper__doc__ 也变成了 wrapper 函数的文档字符串,而不是原始 say_hello 函数的。

解决方法

Python提供了 functools.wraps 装饰器来解决这个问题。functools.wraps 可以将原始函数的元数据复制到 wrapper 函数上。

import functools

def my_decorator(func):
    @functools.wraps(func)
    def wrapper():
        """这是wrapper函数的文档字符串"""
        print("在函数执行前做一些事情")
        func()
        print("在函数执行后做一些事情")
    return wrapper

@my_decorator
def say_hello():
    """这是say_hello函数的文档字符串"""
    print("Hello!")

print(say_hello.__name__)  
print(say_hello.__doc__)  

在上述代码中,使用 @functools.wraps(func) 装饰 wrapper 函数后,say_hello 函数的 __name____doc__ 恢复为原始值。

装饰器的局限性

虽然装饰器是一个非常强大的工具,但它也有一些局限性。

调试困难

由于装饰器会改变函数的执行流程,在调试被装饰的函数时可能会变得更加困难。例如,当函数出现错误时,错误信息可能指向装饰器内部的 wrapper 函数,而不是原始函数,这使得定位问题变得复杂。

可读性问题

过多地使用装饰器,特别是多层嵌套的装饰器,可能会降低代码的可读性。其他人在阅读代码时,可能需要花费更多的时间来理解装饰器的逻辑以及它们对原始函数的影响。

性能开销

装饰器本身会带来一定的性能开销,特别是在装饰器内部执行复杂逻辑时。每次调用被装饰的函数,都需要经过装饰器的逻辑,这可能会影响程序的整体性能,尤其是在对性能要求较高的场景中。

尽管存在这些局限性,但在适当的场景下,装饰器仍然是Python编程中非常有用的工具,可以大大提高代码的可维护性和可扩展性。通过合理使用装饰器,并注意它们的局限性,可以编写出高质量、易于理解和维护的Python代码。