什么是python的装饰器,如果你想知道的话,我就带你去研究!

装饰器

装饰器是个啥?
要了解装饰器,自然要先明白它到底是个啥东西啦。简单来说,装饰器其实就是一个函数,这个函数接受其他函数作为参数,并将其以一个新的修改后的函数作为替换。
举个例子?
让我们来举个例子以更好地理解上面那句话的含义。比方说我们想在某个函数执行前都打印一次pikachu字符,那么我们可以这样做:

def logo(func):
    def wrapper(*args):
        print('''
_____ _ _              _           
|  __ (_) |            | |          
| |__) || | ____ _  ___| |__  _   _ 
|  ___/ | |/ / _` |/ __| '_ \| | | |
| |   | |   < (_| | (__| | | | |_| |
|_|   |_|_|\_\__,_|\___|_| |_|\__,_|''')
        return func(*args)
    return wrapper

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

def multiply(a, b):
    return a * b

add_wrapper = logo(add)
multiply_wrapper = logo(multiply)
print(add_wrapper(2, 5))
print(multiply_wrapper(2, 5))

运行上面的代码后的效果如下:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第1张图片
使用python提供的装饰器语法,则上面的代码可以写成:

def logo(func):
    def wrapper(*args):
        print('''
_____ _ _              _           
|  __ (_) |            | |          
| |__) || | ____ _  ___| |__  _   _ 
|  ___/ | |/ / _` |/ __| '_ \| | | |
| |   | |   < (_| | (__| | | | |_| |
|_|   |_|_|\_\__,_|\___|_| |_|\__,_|''')
        return func(*args)
    return wrapper

@logo
def add(a, b):
    return a - b

@logo
def multiply(a, b):
    return a * b

print(add(2, 5))
print(multiply(71, 5))

运行上面的代码后得到的效果如下(即和第一版代码的效果是一样的):
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第2张图片
通过这个例子,相信聪明的小伙伴们已经对装饰器有了一个大概的认识了。即装饰器其实就是一个函数,这个函数接受其他函数作为参数,并将其以一个新的修改后的函数作为替换。
为什么要使用装饰器?
其实上面的例子已经隐含了这个问题的答案。即当我们想为很多不同的函数添加相同的功能时(例如计时、保存日志等等),我们可以利用装饰器来使得我们的代码更加整洁(或者说pythonic?)
当然,如果你认为装饰器的用法仅限于此,那你就大错特错了。只要脑回路足够多,我们就可以利用python的装饰器创造"无限的"可能。这里举个简单的例子吧:

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

上面的函数很简单,功能就是计算斐波那契数列,我们可以看一下它的运行时间是多久:
在这里插入图片描述
看起来好慢的样子?这是因为上面这段代码存在一个问题,即你想要计算fib(30),那你就需要计算fib(28)和fib(29),而计算fib(29)的时候,你还得再算一遍fib(27)和fib(28),显然,这里fib(28)被重复计算了一次。以此类推,上面这段代码中是存在很多重复计算的。现在,我们尝试借助装饰器来解决这个问题(即利用装饰器来存储运算的中间结果以避免重复运算):
def memory(func):

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

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

现在,我们再来查看一下它的运行时间:
在这里插入图片描述
我们可以发现运行时间直接从微秒级别下降到了纳秒级别(我没有在暗示光刻机图片)。
类装饰器
看到"类装饰器"这个词,很多小伙伴可能会发问了:"你前面不是说装饰器其实就是个函数吗?咋又成类了?“害,上面这么说是为了方便大家快速理解装饰器这个概念嘛,不然一股脑地抛出所有东西,很容易让人丈二和尚摸不着头脑哒~
现在,我们来纠正一下前面的说法,即并非只有函数对象可以作为装饰器使用,事实上,在python中,某个对象能否作为装饰器形式使用,只有一个要求,即该对象必须是一个"可被调用(callable)的对象”。显然,函数肯定是可被调用的。而对于类来说,我们则可以通过定义类的__call__这个魔法方法,来使得它可调用。举个例子:

import functools

class MemoryClass():
    def __init__(self, is_logging, func):
        self.is_logging = is_logging
        self.func = func
        if self.is_logging:
            print('关注小编,一键三连哟~~')
        self.cache = {
     }
    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

def memory(is_logging):
    return functools.partial(MemoryClass, is_logging)

@memory(is_logging=True)
def fib(n):
    if n <= 1: return 1
    return fib(n-1) + fib(n-2)

print(fib(50))

运行结果如下:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第3张图片
显然,相比函数装饰器,类装饰器具有更加灵活,封装性更好等优点~
使用装饰器会带来哪些问题?
显然,根据辩证法,所有事物都是有利有弊的。那么我们使用装饰器的过程中可能会存在哪些问题呢?我们又该如何解决这些问题呢?别急,我们慢慢说~
(1) 函数的属性会发生变化
如前所述,装饰器用新函数来替换了原来的旧函数。那么显然,这个新函数就会缺少很多旧函数的属性,举个例子:

def advertising(func):
    def wrapper(*args):
        print('关注小编,一键三连哟~~')
        return func(*args)
    return wrapper

@advertising
def add_1(a, b):
    '''加法运算'''
    return a + b

def add_2(a, b):
    '''加法运算'''
    return a + b

print('add_1文档: ', add_1.__doc__)
print('add_1函数名: ', add_1.__name__)
print('add_2文档: ', add_2.__doc__)
print('add_2函数名: ', add_2.__name__)

上面代码的运行结果如下:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第4张图片
显然,我们可以发现,使用了装饰器之后,我们无法正确地获取函数原有的文档和名字了。该问题的解决方案如下:

import functools

def advertising(func):
    @functools.wraps(func)
    def wrapper(*args):
        print('关注小编,一键三连哟~~')
        return func(*args)
    return wrapper

@advertising
def add_1(a, b):
    '''加法运算'''
    return a + b

print('add_1文档: ', add_1.__doc__)
print('add_1函数名: ', add_1.__name__)

重新运行代码,获得的结果如下:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第5张图片
完美解决~
(2) 函数参数的获取
先来看一段代码(这里我们想利用装饰器来保证除法运算的除数不是一个接近零的数):

import functools

def check(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if kwargs.get('divisor') <= 1e-6 and kwargs.get('divisor') >= -1e-6:
            raise ValueError('除数不能为0!')
        return func(*args)
    return wrapper

@check
def division(dividend, divisor):
    return dividend / divisor

print(division(5, 1))

运行之后发现报错:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第6张图片
为什么呢?这是因为我们传入的除数(divisor)是一个位置参数,而我们却用关键字参数来获取它了。该问题的解决方案如下:

import inspect
import functools

def check(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        getcallargs = inspect.getcallargs(func, *args, **kwargs)
        if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:
            raise ValueError('除数不能为0!')
        return func(*args)
    return wrapper

@check
def division(dividend, divisor):
    return dividend / divisor

现在我们就可以正常运行上面的代码啦:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第7张图片
(3) 修改外层变量
假设我们想看下函数被调用的次数,那么我们也许会把代码写成这个样子:

import inspect
import functools

def check(func):
    count = 0
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        count += 1
        print(f'第{
       count}次调用')
        getcallargs = inspect.getcallargs(func, *args, **kwargs)
        if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:
            raise ValueError('除数不能为0!')
        return func(*args)
    return wrapper

@check
def division(dividend, divisor):
    return dividend / divisor

print(division(5, 1))

但是上面这个代码运行时会报错:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第8张图片

出错的原因是当解释器执行到count += 1时,并不知道count是一个在外层作用域定义的变量,它把count当成局部变量在当前作用域内进行查找了。最终因为没找到count变量相关的任何定义而抛出错误。为了解决这个问题,我们可以这样写:

import inspect
import functools

def check(func):
    count = 0
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        nonlocal count
        count += 1
        print(f'第{
       count}次调用')
        getcallargs = inspect.getcallargs(func, *args, **kwargs)
        if getcallargs.get('divisor') <= 1e-6 and getcallargs.get('divisor') >= -1e-6:
            raise ValueError('除数不能为0!')
        return func(*args)
    return wrapper

@check
def division(dividend, divisor):
    return dividend / divisor

print(division(5, 1))

即通过nonlocal关键字来告诉解释器count变量并不属于当前作用域,可以到外面找找这个变量的定义。由此,我们的代码就可以正常运行啦:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第9张图片

使用多个装饰器
就像很多人发自拍一样,必须化妆品和美颜相机一起用才能体现自己的颜值(好像必须声明一下,号主是男生!!!),那么我们该如何使用多个装饰器去"装饰"某个函数呢?
很简单,我们只需要这样做:

def DecoratorA(func):
    def wrapper(*args):
        print('先吃饭')
        return func(*args)
    return wrapper

def DecoratorB(func):
    def wrapper(*args):
        print('再睡觉')
        return func(*args)
    return wrapper

@DecoratorA
@DecoratorB
def add(a, b):
    return a + b

print(add(1314, 2020))

运行效果如下:
什么是python的装饰器,如果你想知道的话,我就带你去研究!_第10张图片
好啦,今天的内容就是这些啦~
大家有啥不懂的可以直接在评论区评论哟,或者私信小编就好啦~~

你可能感兴趣的:(python,python,编程语言,设计模式,函数闭包)