ljzsdut
GitHubToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

14.1 装饰器

此篇摘自网络

装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象,并把这个函数对象赋值给被修饰的函数名。

装饰器模板

先上一个如何定义装饰器的模板:

def FUNC_NAME(func):
    def wrapper(*args,**kwargs): 
        # 需要修改的代码,为被修饰的函数增加功能
        #logging.warning("{} is running".format(func.__name__))  
        return func(*args,**kwargs)
    return wrapper

如果要在被修饰函数执行后增加一些功能,可以使用如下方法:

def FUNC_NAME(func):
    def wrapper(*args,**kwargs): 
        tmp = func(*args,**kwargs)
        # 需要修改的代码,为被修饰的函数增加功能
        #logging.warning("{} is finish".format(func.__name__)) 
        return tmp
    return wrapper

如何使用:在被修饰函数之前使用@FUNC_NAME

@FUNC_NAME
def foo():
  pass

装饰器的引入

讲 Python 装饰器前,我想先举个例子,虽有点污,但跟装饰器这个话题很贴切。

每个人都有的内裤主要功能是用来遮羞,但是到了冬天它没办法为我们防风御寒,咋办?我们想到一个办法就是把内裤改造一下,让他变得更厚更长,这样一来,它不仅有遮羞功能,还能提供保暖,不过有个问题,这个内裤被我们改造成长裤后,虽然还具有遮羞功能,但本质上他不再是一条真正的内裤了。于是聪明的人们发明了长裤,在不修改内裤的前提下,直接将长裤套在了内裤的外面,这样内裤还是内裤,有了长裤也可以防风御寒了。装饰器就像我们这里说的长裤,在不影响内裤作用的前提下,给我们的身子提供了保暖的功效。

谈装饰器前,还要先要明白一件事,Python 中的函数和 Java、C++不太一样,Python 中的函数可以像普通变量一样当做参数传递给另外一个函数,例如:

def foo():
    print("foo")
def bar(func):
    func()
    
bar(foo)

正式回到我们的主题。装饰器本质上是一个Python函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

先来看一个简单例子,虽然实际代码可能比这复杂很多:

def foo():
    print("i am foo")

现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:

def foo():
    print("i am foo")
    logging.info("foo is running")

如果函数bar()、bar2()也有类似的需求,怎么做?再写一个logging在bar函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志,日志处理完之后再执行真正的业务代码:

def use_logging(func):
    logging.warning("{} is running".format(func.__name__))
    func()
    
def foo():
    print("i am foo")

use_logging(foo)

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑foo函数,而是换成了 use_logging函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个foo函数作为参数传递给 use_logging函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。

简单装饰器:

def use_logging(func):
    def wrapper():   #重新定义了被装饰函数
        logging.warning("{} is running".format(func.__name__))
        return func()     #返回函数执行结果
    return wrapper  #将重新定义的函数名返回,赋值给了被装饰的函数

def foo():
    print("i am foo")

foo=use_logging(foo) #因为装饰器use_logging(foo)返回的是函数对象wrapper,所以这条语句相当于foo=wrapper
foo()

use_logging就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数func包裹在其中,看起来像foo被use_logging装饰了一样,use_logging返回的也是一个函数,这个函数的名字叫wrapper。在这个例子中,函数进入和退出时,被称为一个横切面,这种编程方式被称为面向切面的编程。

@ 语法糖:

如果你接触 Python 有一段时间了的话,想必你对 @ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。即@语法糖等同于foo=use_logging(foo)

def use_logging(func):
    def wrapper():   #重新定义了被装饰函数
        logging.warning("{} is running".format(func.__name__))
        return func()     #返回函数执行结果
    return wrapper  #将重新定义的函数名返回,赋值给了被装饰的函数

@use_logging
def foo():
    print("i am foo")

foo()

如上所示,有了@,我们就可以省去foo=use_logging(foo)这一句了`,直接调用foo()即可得到想要的结果。你们看到了没有,foo()函数不需要做任何修改,只需在定义的地方加上装饰器,调用的时候还是和以前一样,如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

​ 装饰器在Python使用如此方便都要归因于Python的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。

业务函数的参数处理

可能有人问,如果我的业务逻辑函数foo需要参数怎么办?比如foo函数定义如下:

def foo(name):
    print("i am {}".format(name))

我们可以在定义wrapper函数的时候指定参数:

def use_logging(func):
    def wrapper(name):   #装饰器下一层函数,会被赋值给原函数(被装饰函数),所以他的参数也会接受原函数的参数
        logging.warning("{} is running".format(func.__name__))
        return func(name)
    return wrapper

@use_logging       #此行等同于在函数foo后面使用:foo=use_logging(foo)
def foo(name='foo'):
    print("i am {}" .format(name))

foo("python")

这样foo函数定义的参数就可以定义在wrapper函数中。这时,又有人要问了,如果foo函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道foo到底有多少个参数时,我们可以用*args来代替:

def use_logging(func):
    def wrapper(*args):   #装饰器下一层函数,会被赋值给原函数(被装饰函数),所以他的参数也会接受原函数的参数
        logging.warning("{} is running".format(func.__name__))
        return func(*args)
    return wrapper

@use_logging       #此行等同于在函数foo后面使用:foo=use_logging(foo)
def foo(name='foo',age=18):
    print("i am {},age is {}" .format(name,age))

foo("python",18)

如此一来,甭管foo定义了多少个参数,我都可以完整地传递到func中去。这样就不影响foo的业务逻辑了。这时还有读者会问,如果foo函数调用时赋值了一些关键字参数呢?这时,你就可以把wrapper函数指定关键字函数:

def use_logging(func):
    def wrapper(*args,**kwargs): 
        logging.warning("{} is running".format(func.__name__))
        return func(*args,**kwargs)
    return wrapper

@use_logging       #此行等同于在函数foo后面使用:foo=use_logging(foo),把函数对象带入装饰器函数,装饰后再次放回该函数对象
def foo(name='foo',age=18):
    print("i am {},age is {}" .format(name,age))

#foo=use_logging(foo)

foo("python",age=18)

image-20200304172836984

带参数的装饰器:

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数foo。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

def use_logging(level):
    def decorator(func):
        def wrapper(*args,**kwargs):
            if level== "warn":
                logging.warning("{} is running".format(func.__name__))
            elif level=="info":
                logging.info("{} is running".format(func.__name__))
            return func(*args,**kwargs)
        return wrapper
    return decorator

@use_logging(level="warn")   #带参数的装饰器@use_logging(level="warn")等价于foo=decorator(func)=use_logging(level="warn")(func)等价于@decorator,将被装饰的函数名传递给装饰函数的下一层函数decorator
def foo(name='foo',age=18):
    print("i am {},age is {}" .format(name,age))

foo('python',20)

上面的use_logging是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。我们可以将它理解为一个含有参数的闭包。当我们使用@use_logging(level=“warn”)调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中(将函数名func参数传递给下一层的函数)。@use_logging(level=“warn”)等价于@decorator

示例:统计函数执行时间

def TimeConsuming(func):
    def wrapper(*args,**kwargs):
        t1 = time.time()
        tmp=func(*args,**kwargs)
        t2=time.time()
        print("函数{}执行耗时{}s".format(func.__name__,t2-t1))
    return wrapper

@TimeConsuming
def func(id):
    time.sleep(2)
    print(id)

类装饰器:

没错,装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的call方法,当使用@形式将装饰器附加到函数上时,就会调用此方法。