阅读 393

Python装饰器,读完这篇你就懂了

这是我参与8月更文挑战的第4天,活动详情查看:8月更文挑战

说起来装饰器,其实算是Python中比较难理解的知识点之一了,在Python开发的面试过程中,这是一个高频问题,甚至说必问问题都不为过,至少我经历的面试是这样。装饰器也是Python开发中经常要使用到的功能,熟练掌握装饰器会让你的代码更加简洁,思路也更加广阔。 好了,废话不多说,直接开锤。

什么是装饰器

所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。理解起来好像就是装饰器本身可以增强其他函数的功能,一个函数平平无奇,没有关系,有我装饰器,定能让你变的闪闪发光。 这么说可能也有点绕,举个栗子,比如你是一个男程序员,然后你带上假发,穿上女装,再画个妆,那你可能会成为一个女装大佬,那么假发、女装、化妆就是装饰器,你还是你,但是因为有了这些装饰器,你成为了一个女装大佬。enmmmm...,大概就是这样。

下面看一个世界上最简单的函数,放宽心,绝对不是Hello World!

def hello():
    print("你好,装饰器!")
复制代码

哈哈,我没骗人吧,不信的小伙伴可以私下里运行一下哈哈哈。

那如果我想让hello()函数再实现个其他的功能,比如打印个执行函数时的时间。 有人说可以在hello()函数里加上打印时间的代码不就好了。

import datetime


def hello():
    print("当前时间:", datetime.datetime.now())
    print("你好,装饰器!")
复制代码

那么问题来了,如果还有hello1()hello2()hello3()等等大量的函数,那也一个一个的加上吗,那就会造成大量的重复代码,为了避免重复代码,我们可以重新定义一个函数,专门用来打印当前时间,打印完时间再执行真正的业务代码。

def print_time(func):
    print("当前时间:", datetime.datetime.now())
    func()


def hello():
    print("你好,装饰器!")


print_time(hello)
复制代码

这样逻辑上虽然没多大毛病,但是这样的话,我们每次都要传递一个函数给print_time()函数,这种做法破坏了原有的代码逻辑结构,原来执行业务逻辑,我们只需要调用hello()函数,而现在我们必须要调用print_time()函数才行,那有没有更好的方式,当然,那就是装饰器。

装饰器的实现

下面就是装饰器方式的实现:

import datetime


def my_decorator(func):
    def wrapper():
        print("当前时间:", datetime.datetime.now())
        func()
    return wrapper


@my_decorator
def hello():
    print("你好,装饰器!")
    
    
hello()
复制代码

运行结果:

当前时间: 2021-07-31 11:31:46.630720
你好,装饰器!

Process finished with exit code 0
复制代码

注:@符号是装饰器的语法糖,在定义函数的时候使用,可以避免再一次赋值操作
此处@my_decorator,相当于

hello = my_decorator(hello) 
hello()
复制代码

很显然,这样不但可以实现需要的功能,代码也更加简洁。 解释一下,当运行最后的hello()函数时,调用过程是这样的:

  • 先是执行hello = my_decorator(hello),此时变量hello指向的是my_decorator()
  • my_decorator(func)中传参是hello,返回的wrapper,而wrapper又会调用到原函数hello()
  • 所以,先执行wrapper()函数里的代码,然后才执行hello()函数里的。

上述代码里的my_decorator()就是一个装饰器,它“增强”了hello()的功能,但是并没有去真正的改变hello()函数的内部实现。

带参数的装饰器

上面的只是一个非常简单的装饰器,但是实际场景中,很多函数都是要带有参数的,比如hello(name)等,那要怎么实现呢,其实也很简单,直接在对应装饰器的wrapper()上,加上对应的参数就可以了。

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("当前时间:", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator
def hello(name):
    print("你好,{}".format(name))


hello("tigeriaf")
复制代码

输出:

当前时间: 2021-07-31 13:43:59.192468
你好,tigeriaf

Process finished with exit code 0
复制代码

注:在python里,*args**kwargs表示接受任意数量和类型的参数,想要了解的可以看我之前的文章python中的*args和**kwargs用法解读

自定义参数

上面的装饰器都是接收外来的参数,其实装饰器有自己的参数。 比如,加个参数来控制下业务函数执行的次数:

def my_decorator_outer(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            print("当前时间:", datetime.datetime.now())
            for i in range(num):
                func(*args, **kwargs)
        return wrapper
    return my_decorator


@my_decorator_outer(3)
def hello(name):
    print("你好,{}".format(name))


hello("tigeriaf")
复制代码

注意:这里是三层嵌套的函数,最外层my_decorator_outer函数的参数num控制的是次数。

运行后会输出以下信息:

当前时间: 2021-07-31 14:37:28.190077
你好,tigeriaf
你好,tigeriaf
你好,tigeriaf

Process finished with exit code 0
复制代码

内置装饰器@functools.wrap

现在我们多做一步探索,我们来打印一下下面例子中的hello()函数的元信息:

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("当前时间:", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator
def hello(name):
    print("你好,{}".format(name))


# hello("tigeriaf")
print(hello.__name__)  # 打印hello函数的元信息
复制代码

结果为:

wrapper

Process finished with exit code 0
复制代码

这说明,它不再是以前的那个hello()函数,而是被wrapper()函数取代了,函数名等函数属性会发生改变,当然这对结果不会产生什么影响,但是如果我们需要用到元函数信息,那要怎么保留它呢?哎,这时候可以用内置装饰器@functools.wrap来实现。

import functools


def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("当前时间:", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator
def hello(name):
    print("你好,{}".format(name))


# hello("tigeriaf")
print(hello.__name__)
复制代码

运行下看看:

hello

Process finished with exit code 0
复制代码

显然,我们在写一个装饰器的时候,最好加上@functools.wrap,它能保留原有函数的名称和函数属性。

类装饰器

刚刚我们接触的装饰器是函数来完成,由于Python的灵活性,我们也可以使用类来实现一个装饰器。
类能实现装饰器的功能,是由于当我们调用一个类的实例时,就调用到它的__call__()方法。
那接下来我们把之前的例子改成类装饰器来实现:

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

    def __call__(self, *args, **kwargs):
        print("当前时间:", datetime.datetime.now())
        return self.func(*args, **kwargs)


@MyDecorator
def hello(name):
    print("你好,{}".format(name))


hello("tigeriaf")
复制代码

运行结果:

当前时间: 2021-07-31 15:06:15.400093
你好,tigeriaf

Process finished with exit code 0
复制代码

多层装饰器的执行顺序

既然装饰器可以增强函数的功能,那如果有多个装饰器,我都想要怎么办?
其实,只要把需要用的装饰器都加上去就好了。

@decorator1
@decorator2
@decorator3
def hello():
    ...
复制代码

但是要注意的是,这里的执行顺序,会从上到下依次执行,可以来看下:

def my_decorator1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("装饰器1,当前时间:", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


def my_decorator2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("装饰器2,当前时间:", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


def my_decorator3(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print("装饰器3,当前时间:", datetime.datetime.now())
        func(*args, **kwargs)
    return wrapper


@my_decorator1
@my_decorator2
@my_decorator2
def hello(name):
    print("你好,{}".format(name))


hello("tigeriaf")
复制代码

运行结果:

装饰器1,当前时间: 2021-07-31 15:13:23.824342
装饰器2,当前时间: 2021-07-31 15:13:23.824342
装饰器2,当前时间: 2021-07-31 15:13:23.824342
你好,tigeriaf

Process finished with exit code 0
复制代码

总结

好了,关于装饰器就先说道这里,其实装饰器经常用于有切面需求的场景,比如:性能测试、添加日志、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与业务函数功能本身无关的重复代码,这样可以使整体更简洁。何乐而不为呢。

最后,感谢女朋友在工作和生活中的包容、理解与支持 !

文章分类
后端
文章标签