Python装饰器(一次搞清楚)

发布时间 2023-10-09 21:24:43作者: 煊奕
最重要的情绪管理是要明白,没有一种情绪是不应该的

一、简单装饰器

Python装饰器是一种语法糖,用于在不改变原有函数代码的情况下,为函数添加额外的功能。装饰器本质上是一个函数,它接收一个函数作为参数,并返回一个新的函数,通常使用@语法糖来应用装饰器。
1.装饰器本质是一个函数,可称之为函数装饰器;
2.装饰器也是一个闭包,即在非全局范围内定义的函数可以引用其外围空间中的变量
3.装饰器以一个函数作为参数,并且返回值也是一个函数;
4.装饰器不能修改被装饰的函数代码;不能修改被装饰函数的调用方式;
下面是一个简单的装饰器示例:

def my_decorator(func):
    def wrapper():
        print("Before the function is called.")
        res = func()
        print("After the function is called.")
        return res
    return wrapper

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

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

在这个示例中,my_decorator是一个装饰器函数,它接收一个函数作为参数,并返回一个新的函数wrapper。wrapper函数包裹了原有的函数,它在调用原有函数之前和之后打印了额外的信息。@my_decorator语法糖将say_hello函数传递给my_decorator,并将其返回的新函数wrapper赋值给say_hello,这样调用say_hello函数时,实际上是调用了wrapper函数。执行结果为:
image.png

二、装饰器的运用场景

装饰器的应用非常广泛,可以用于实现各种功能,例如:

  1. 记录函数执行时间:通过在装饰器函数中记录函数执行的开始和结束时间,可以计算函数的执行时间。
  2. 缓存函数结果:通过在装饰器函数中维护一个缓存,可以避免重复计算相同参数的函数结果,提高函数的执行效率。
  3. 实现权限控制:通过在装饰器函数中检查用户的权限,可以控制用户对某些函数的访问权限。
  4. 日志记录:通过在装饰器函数中记录函数的输入参数和返回值,可以方便地进行调试和错误排除。
  5. 错误处理:通过在装饰器函数中捕获异常并进行处理,可以避免函数抛出异常导致程序崩溃。

Python装饰器是一种非常强大的语法糖,可以帮助我们实现各种功能,提高代码的复用性和可维护性。

三、运用案列

案列一:Python缓存cache实现

Python中的缓存(cache)机制可以通过装饰器来实现,但并不是所有的缓存都是通过装饰器实现的。装饰器是一种常用的实现缓存的方式,但是Python中还有其他的缓存实现方式,例如使用字典、使用缓存库等。
使用装饰器实现缓存的原理是,在装饰器函数中维护一个字典,将函数的输入参数作为键,函数的返回值作为值,存储在字典中。在每次调用函数之前,先检查字典中是否已经存在相同输入参数的缓存结果,如果存在,则直接返回缓存结果,否则调用原函数计算结果,并将计算结果缓存到字典中。
下面是一个简单的装饰器缓存示例:

def cache(func):
    cached_results = {}
    def wrapper(*args):
        if args in cached_results:
            return cached_results[args]
        result = func(*args)
        cached_results[args] = result
        return result
    return wrapper

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

print(fibonacci(10))

1.在这个示例中,cache是一个装饰器函数,它接收一个函数作为参数,并返回一个新的函数wrapper。wrapper函数维护了一个cached_results字典,用于存储函数的缓存结果。在调用被装饰的函数之前,wrapper函数先检查输入参数args是否已经存在于cached_results中,如果存在,则直接返回缓存结果,否则调用原函数计算结果,并将结果存储到cached_results中。
2.这个装饰器可以用于缓存计算复杂的函数,例如计算斐波那契数列的函数fibonacci。在第一次调用fibonacci(10)时,由于没有缓存结果,需要进行计算,计算结果存储到字典中。在后续的调用中,只需要从字典中获取结果即可,避免了重复计算,提高了程序的执行效率。
3.需要注意的是,装饰器缓存的实现方式可能存在一些问题,例如缓存数据的过期问题、内存占用问题等。因此,在实际使用中需要根据具体情况进行选择和调整,以确保程序的正确性和性能。

案列二:打印函数执行时间消耗

import time
# 注意:这个是带参数的装饰器
def calculate_execution_time(unit='s'):
    def decorator(func):
        def wrapper(*args, **kwargs):
            start_time = time.time()
            result = func(*args, **kwargs)
            end_time = time.time()
            execution_time = end_time - start_time
            if unit == 'ms':
                execution_time *= 1000
                print(f"函数 {func.__name__} 的执行时间为: {execution_time:.2f} 毫秒")
            else:
                print(f"函数 {func.__name__} 的执行时间为: {execution_time:.2f} 秒")
            return result
        return wrapper
    return decorator

@calculate_execution_time(unit='ms')
def my_function(a, b):
    # 假设这里是一个耗时的操作
    time.sleep(1)
    # 被装饰删数本身的功能
    print("{0} + {1} = ".format(a, b), a+b)

my_function(1, 2)

1.上述代码定义了一个名为calculate_execution_time的装饰器,它接受一个可选的参数unit,用于指定执行时间的单位,默认为秒。装饰器内部定义了另一个函数decorator,它接受被装饰的函数func作为参数。在wrapper函数中,我们首先记录函数执行开始的时间戳start_time,然后调用被装饰的函数并获取其返回值result,最后计算函数执行所花费的时间,并根据unit参数选择合适的单位进行打印。
2.执行结果:
image.png
3.带参数的装饰器详见下文。

四、带参数的装饰器

Python装饰器可以用于装饰任何可调用对象,包括函数和类。当装饰函数带参数时,需要在装饰器函数里再定义一层函数来接收参数,这样才能将参数传递给被装饰的函数。
下面是一个简单的装饰器示例,演示了如何在装饰器函数中处理带参数的函数:

from functools import wraps
def repeat(num):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for i in range(num):
                print(f"Running function {func.__name__} for the {i+1} time")
                func(*args, **kwargs)
        return wrapper
    return decorator

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

greet("John")

1.在这个示例中,repeat是一个装饰器函数,它接收一个参数num,用于指定函数重复执行的次数。decorator是repeat的内部函数,它接收一个函数func作为参数,并返回一个新的函数wrapper。wrapper函数实现了函数的重复执行功能,它在循环中调用被装饰的函数func,并打印执行次数的信息。
另外,@wraps(func)用于保存函数的元信息,例如函数名、参数、注释等,可以使用functools模块中的wraps装饰器来保存被装饰函数的原信息。
2.wraps装饰器实际上是一个装饰器工厂函数,它接收一个函数作为参数,并返回一个新的装饰器函数。这个新的装饰器函数会将被装饰的函数替换为自己,并使用functools模块中的update_wrapper函数来将被装饰函数的元信息复制到新的装饰器函数中。
3.如果不使用wraps装饰器来保存被装饰函数的元信息,那么被装饰函数的元信息会被覆盖,例如函数名会变成wrapper,函数注释会变成装饰器函数的注释。
4.在应用装饰器时,使用@语法糖将装饰器函数repeat(3)应用到函数greet上,例如:

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

这样,每次调用greet函数时,都会执行3次,打印出执行的次数和函数的输出结果。
5.需要注意的是,在装饰器函数中定义的参数,需要在装饰器的每一层函数中进行传递和处理。在这个示例中,num参数在repeat函数中定义,被传递给decorator函数,最终在wrapper函数中使用。被装饰的函数的参数,需要在wrapper函数中定义为*args**kwargs,以支持任意数量和类型的参数,并在调用被装饰的函数时传递给它。
6.执行结果:
image.png

五、装饰器执行过程

像上面的函数,在代码执行时,会首先将**@cache**加载(函数定义时),执行cache中的内容,不执行wrapper的内容(函数调用时)。后续fibonacci函数执行时,会先执行对应的wrapper的内容,再执行函数本身,即cache中的cached_results类似于wrapper的全局变量
image.png
后续的多次调用中,cache中仅wrapper的内容在执行
image.png

六、装饰器的执行顺序

说法一:装饰顺序按由下到上,调用时由上到下执行顺序和装饰顺序相反。(简单记成正常的代码顺序即可)
说法二:装饰器由下到上依次立马执行,之后我们调用的f已经是被装饰器执行了之后的f了,此时是由上到下返回去依次调用。整个过程有点像先上楼梯(装饰过程),再下楼梯(调用函数)

def decorator_a(func):
    print('Get in decorator_a')
    def inner_a(*args, **kwargs):
        print('Get in inner_a')
        return func(*args, **kwargs)
    return inner_a

def decorator_b(func):
    print('Get in decorator_b')
    def inner_b(*args, **kwargs):
        print('Get in inner_b')
        return func(*args, **kwargs)
    return inner_b

@decorator_b
@decorator_a
def f(x):
    print('Get in f')
    return x * 2

f(1)

执行结果:
image.png
关于执行顺序可参考:https://segmentfault.com/a/1190000007837364