Python高级编程之装饰器Decorator详解(上篇)***

目录

一、先从一种情况开始看起

1、装饰器decorator的由来

2、添加额外功能的简单实现——非“装饰器”实现

二、什么是装饰器——decorator

1、什么是装饰器?——两个层面

第一:从设计模式的层面上

第二:从Python的语法层面上(其实第二种本质上也是第一种,只不过在语法上进行了规范化)

2、装饰器的作用——两方面

3、装饰器的使用场景

三、装饰器的实现

1、装饰器的逐步实现

2、装饰器的一般结构

四、装饰器的各种花式实现

函数装饰函数

函数装饰类

类装饰函数

类装饰类

例子:

想要带参数的装饰器,则需要再多加一层函数嵌套:


一、先从一种情况开始看起

1、装饰器decorator的由来


装饰器的定义很是抽象,我们来看一个小例子。
先定义一个简单的函数:

def myfunc:
    print('我是函数myfunc')


myfunc()  #调用函数
然后呢,我想看看这个函数执行这个函数用了多长时间,好吧,那么我们可以这样做:

import time
def myfunc:
    start = time.clock()  
    print('我是函数myfunc')
    end = time.clock()
    print(f'函数所花费的时间为 :{end - start}')


 
myfunc()  #函数调用
我们现在已经达到了我们的目的。但是如果是我们还想继续给另外的一些函数也实现同样的功能。那我们是不是给每个函数都添加这么几句话呢?当然可以,但是不高效,而且很麻烦。如果有某一种方式可以一次性解决所有的问题,那自然最好不过了,于是“装饰器”就应运而生。

在上面的例子中,函数本身的功能只是打印一句话而已,但是经过改造后的函数不仅要能够打印这一句话,还要能够显示函数执行所花费的时间,这相当于我要给这个函数添加额外的功能,注意这个关键字,其实“装饰器”就是专门给函数添加额外的功能的。

2、添加额外功能的简单实现——非“装饰器”实现


还记得吗,函数在Python中是一等公民,那么我们可以考虑重新定义一个函数timeit,将myfunc的引用传递给他,然后在timeit中调用myfunc并进行计时,这样,我们就达到了不改动myfunc定义但是又添加了额外功能的目的,代码如下:

import time
 
def myfunc():
 
    print("我是函数myfunc")
 
def timeit(function):
    start = time.clock()
    function()
    end =time.clock()
    print(f'函数执行所花费的时间为:{end-start}')


 
timeit(myfunc)
只行结果为:

我是函数myfunc
函数执行所花费的时间为:0.0004924657368762765

上面的代码看起来逻辑上并没有问题,也达到了我们所要实现的目的!但是,我们虽然没有修改函数myfunc定义中的代码,但是我们似乎修改了调用部分的代码。原本我们是这样调用的:myfunc(),修改以后变成了:timeit(myfunc)。这样的话,如果myfunc在N处都被调用了,你就不得不去修改这N处的代码。或者更极端的,考虑其中某处调用的代码无法修改这个情况,比如:这个函数是你交给别人使用的。
其实将函数作为参数传递,已经具备了装饰器的雏形了,但是上面的实现还不够好,下面会给出更好地实现方式。 
 

二、什么是装饰器——decorator

一般而言,如果我需要给函数添加额外的某一些功能,我需要修改函数的源代码,但是如前面所说,这样麻烦,而且不高效,装饰器就是专门的解决方案!

1、什么是装饰器?——两个层面

在Python里面有两层定义:

第一:从设计模式的层面上

装饰器是一个很著名的设计模式,经常被用于有切面需求的场景,较为经典的应用有插入日志、增加计时逻辑来检测性能、加入事务处理等。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量函数中与函数功能本身无关的雷同代码并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能。

第二:从Python的语法层面上(其实第二种本质上也是第一种,只不过在语法上进行了规范化)

简言之,python装饰器就是用于拓展原来函数功能的一种函数,这个函数的特殊之处在于它的返回值也是一个函数,使用python装饰器的好处就是在不用更改原函数的代码前提下给函数增加新的功能。 如此一来,我们要想拓展原来函数代码,就不需要再在函数里面修改源代码了。

2、装饰器的作用——两方面

(1)抽离雷同代码,加以重用

(2)为函数添加额外的功能

3、装饰器的使用场景

(1)缓存装饰器

(2)权限验证装饰器

(3)计时装饰器

(4)日志装饰器

(5)路由装饰器

(6)异常处理装饰器

(7)错误重试装饰器

三、装饰器的实现

1、装饰器的逐步实现

针对上面改进版的代码所存在的哪些问题,我们想出了解决办法:

既然修改N处的调用代码很麻烦,我们就来想想办法不修改调用代码;如果不修改调用代码,也就意味着调用myfunc()需要产生调用timeit(myfunc)的效果。

因为python中一切皆对象,故而我们可以想到将timeit赋值给myfunc,

代码如下:

import time
 
def myfunc():
 
    print("我是函数myfunc")
 
def timeit(function):
    start = time.clock()
    function()
    end =time.clock()
    print(f'函数执行所花费的时间为:{end-start}')



myfunc=timeit  #将timeit赋值给原来的myfunc
myfunc()
但是上面的调用并不会成功,会显示出如下错误:

timeit() missing 1 required positional argument: 'function'

这是因为将timeit赋值给myfunc之后,此时myfunc和timeit表示的同一个东西,但是timeit似乎带有一个参数function,而在调用myfunc()的时候并没有传入任何参数,所以并不会成功。

但是上面的调用虽然没有成功,却给我们指出了一条重要的线索,因为上面的代码已经解决“修改调用代码”的问题,只不过是参数没有统一而已,那就想办法把参数统一吧!那就再添加一个函数呗!什么意思?

因为参数不统一,如果timeit()不并不是直接添加额外的功能,而是返回一个与myfunc参数列表一致的函数。而原来timeit需要添加额外功能的代码再在timeit里面定义一个函数,由它去完成不就可以了吗,将timeit(myfunc)的返回值赋值给myfunc,然后,调用myfunc()的代码完全不用修改。——即我们依然是调用myfunc(调用代码没变),但是同样却达到了添加额外功能的效果!

代码如下:

import time
#原来的函数myfunc
def myfunc():
    print("我是函数myfunc")
 
#定义一个计时器
def timeit(function):
    '''
       timeit函数负责返回一个wrapper,wrapper的参数要与原来的myfunc保持相同
       这样一来,执行 myfunc=timeit(myfunc)  myfunc完全等价于wrapper
       wrapper函数负责添加额外功能
    '''
    def wrapper():
        start = time.clock()
        function()
        end =time.clock()
        print(f'函数执行所花费的时间为:{end-start}')
    return wrapper


 
myfunc=timeit(myfunc)  #注意,这里与前面的 “myfunc=timeit”是有所区别的哦
myfunc()  #还和原来调用myfunc()一样,但是达到了添加额外功能的效果
上面的运行结果就出来了,如下:

我是函数myfunc
函数执行所花费的时间为:0.0005973331798019136
总结:在上面的函数定义和调用中,看起来我的调用myfunc()和原来并没有任何不同,但是却已经添加了额外的效果。它解决前面存在的两个问题:

(1)不用修改函数源代码,也不用修改调用函数的代码,完全跟调用最原始的myfunc()代码一样,但是却添加了额外功能;

(2)解决了timeit和myfunc的参数不统一问题,那就是再添加一层wrapper;

——这就是装饰器。

上面的装饰器就是最原始的版本,但是python中引入了专门的“语法糖”来实现装饰器,这样看起来更加专业,更加美观。就是使用字符“@”去实现。代码如下:

import time
 
#定义一个计时器
def timeit(function):
    '''
       timeit函数负责返回一个wrapper,wrapper的参数要与原来的myfunc保持相同
       这样一来,执行 myfunc=timeit(myfunc)  myfunc完全等价于wrapper
       wrapper函数负责添加额外功能
    '''
    def wrapper():
        start = time.clock()
        function()
        end =time.clock()
        print(f'函数执行所花费的时间为:{end-start}')
    return wrapper


 
#myfunc=timeit(myfunc)  #注意,这里与前面的 “myfunc=timeit”是有所区别的哦
 

#原来的函数myfunc
@timeit
def myfunc():
    print("我是函数myfunc")


 
myfunc()  #还和原来调用myfunc()一样,但是达到了添加额外功能的效果
上面代码的运行结果依然是:

我是函数myfunc
函数执行所花费的时间为:0.0004893814003196401

在上面的例子中,在定义myfunc函数的上面加了一个@timeit,这与前面的写法myfunc = timeit(myfunc)完全等价,

@有两个重要的作用,第一:较少了代码书写量;第二:那就是让我们的代码看上去更有装饰器的感觉,看起来更高端了。

总结:

在这个例子中,函数进入和退出时需要计时,这被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。与传统编程习惯的从上往下执行方式相比较而言,像是在函数执行的流程中横向地插入了一段逻辑。在特定的业务领域里,能减少大量重复代码。面向切面编程还有相当多的术语,这里就不多做介绍,感兴趣的话可以去找找相关的资料(如果有需要,我后面也会抽时间专门写一系列关于面向切面编程的文章,看我有没有时间啦!)

2、装饰器的一般结构

为了能够明确装饰器的实现原理,这里给出一个关于装饰器的“一般模板”,方便大家理解!但是,装饰器作为一种设计模式,本身是没有固定的设计模板的,语法也是相对较为灵活,没有说一定要怎么写才正确。

模板如下:

def decorator(function):
    '''
    第一层函数为装饰器名称
    function:参数,即需要装饰的函数
    return:返回值wrapper,为了保持与原函数参数一致
    '''
    def wrapper(*arg,**args):
        '''
           内层函数,这个函数实现“添加额外功能”的任务
           *arg,**args:参数保持与需要装饰的函数参数一致,这里用*arg和**args代替
        '''
        #这里就是额外功能代码
        function()   #执行原函数
        #这里就是额外功能代码
    return wrapper


一般就按照上面这个模板写“装饰器”函数,一般就不会出错了。
 

四、装饰器的各种花式实现

学过装饰器的人都知道Python的闭包,关于“闭包”的详细定义有各种版本,但我们经常看见这样一句话,“Python的装饰器就是一种闭包或者是Python的闭包其实就是装饰器”,这句话在一定程度上是不正确的,但是这么说也可以(心里要明白二者的本质)。

本质:python闭包是装饰器的真子集,即装饰器是更加宽泛的概念,至于为什么,它们二者的区别和联系,我会在

Python高级编程——装饰器Decorator详解(上篇)

中继续讲解python闭包和装饰器的区别和联系。

不仅如此,上面所实现的装饰器是针对函数的,实际上Python的装饰器可以是“函数”或者是“类”,而被装饰的对象也可以是“函数”或者是“类”,这样一来,就有四种搭配情况,即:

函数装饰函数

函数装饰类

类装饰函数

类装饰类

具体每一种怎么实现呢?其实他们的设计思想都是大同小异,只是实现细节略有不同,欲知详细情况,且听下回分解!!!

下一篇预告:

装饰器与闭包的联系和区别

四大类装饰器的搭配实现

例子:

比如我们有一个函数,下载图片,用装饰器实现timeout之后,自动重新下载一次。

import functools
import random

# 定义当timeout发生时要抛出的异常
class TimeOutError(Exception):
    pass

# 定义装饰器
def retry(func):
    @functools.wrap(func)
    def wrapper(*arg,**kwarg):
        try:
            print("first try...")
            func(*arg,**kwarg)
        except TimeOutError:
            print("timeout occurs, retrying...")
            func(*arg,**kwarg)
  return wrapper  

# 定义download函数
@retry
def download():
    print("downloading the photos...")
    download_time = random.ranint(1,2)
    if download_time>1:
        print("the download_time > 1s, time out")
        raise TimeOutError
    else:
        print("download finished.")

想要带参数的装饰器,则需要再多加一层函数嵌套:

#!/usr/bin/python
#-*- coding:utf-8 -*-

import functools
import random

# 定义当timeout发生时要抛出的异常
class TimeOutError(Exception):
    pass

# 定义装饰器
def decorator_download(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*arg,**kwarg):
            #try:
            #    print("first try...")
            #    func(*arg,**kwarg)
            #except TimeOutError:
            #    print("timeout occurs, retrying...")
            #    func(*arg,**kwarg)
            print(text)
            print("first try...")
            result = func(*arg,**kwarg)
            while result == False:
                print("will retry...")
                result = func(*arg,**kwarg)
        return wrapper
    return decorator

# 定义download函数
@decorator_download("retry until download finished successfully")
def download():
    print("downloading the photos...")
    download_time = random.randint(1,2)
    if download_time>1:
        print("the download_time > 1s, time out")
        #raise TimeOutError
        return False
    else:
        print("download finished.")
        return True
        
if __name__ == "__main__":
    download()

 

你可能感兴趣的:(python进阶)