19.装饰器

 

装饰器一直是我们学习python难以理解并且纠结的问题,想要弄明白装饰器,必须理解一下函数式编程概念,并且对python中函数调用语法中的特性有所了解,使用装饰器非常简单,但是写装饰器却很复杂。为了讲清楚装饰器,我们讲一个生动的例子如下(由于后文装饰器的代码例子,全程和这个形象生动的说明很一致,所以务必看懂这个例子):

   举个简单的例子:假设很久之前你写过一个函数,现在你突然有了个想法就是你想看看,以前那个函数在你数据集上的运行时间是多少,这时候你可以修改之前代码为它加上计时的功能,但是这样的话是不是还要大体读读你之前的这个的代码,稍微搞清楚一点它的逻辑,才敢给它添加新的东西。这样是不是很繁琐,要是你之前写的代码足够乱足够长,再去读它是不是很抓狂...。实际工作中,我们常常会遇到这样的场景,可能你的需求还不只是这么简单。那么有没有一种可以不对源码做任何修改,并且可以很好的实现你所有需求的手段呢?答案当然是有,这就是要介绍的python装饰器。有了装饰器,你除了不用担心前面提到的问题,并且还可以很好的处理接下来要做的事:那就是现在你又有了一个新的需求,比如为另一个函数添加计时功能,这时就非常简单了,把要装饰的函数丢给装饰器就好了,它会自动给你添加完功能并返回给你。

  所以说:装饰器本质上就是一个函数,它可以让其他函数在不需要做任何代码变动的前提下增加额外的功能,装饰器的返回值也是一个函数对象(函数的指针)

   那么要想理清楚装饰器,我们必须先了解下面几样东西哦,如果对这些全部了解的话,那么装饰器就很简单了。

 

1,函数

  函数是什么呢?

  在python中,使用关键字def和一个函数名以及一个可选的参数列表来定义函数,函数使用return关键字来返回值,我们一般说的函数是指函数名,比如foo,而foo()已经执行函数了,foo()是什么类型取决于return的内容是什么类型!!!

  函数的作用是什么?

 

减少重复代码
 
使程序变得可扩展
 
使程序变得易维护

 

 

2,函数作用域

  在python函数中会创建一个新的作用域,python高手也会称函数有自己的命名空间,也就是说,当函数体中遇到变量时候,python首先会在该函数的命名空间中寻找变量名,python中有几个函数用来查看命名空间。

3,函数即对象

  在python的世界里,函数和[1,2,3],'abc',8等一样都是对象,而且函数是最高级的对象(对象是类的实例化,可以调用相应的方法,函数是包含变量对象的对象)。

  函数对象的调用仅仅比其他对象多了一个()而已,foo,bar和a,b一样都是个变量名,为什么函数只有加载到内存里面才可以被调用?

  注意:这里说的函数都是指函数名,比如foo;而foo()已经执行函数了,foo()是什么类型取决于return的内容是什么类型!!!

既然函数是对象,那么自然满足下面两个条件:

 

 

函数对象的调用仅仅比其它对象多了一个()而已!foo,bar与a,b一样都是个变量名。

那上面的问题也就解决了,只有函数加载到内存才可以被调用。

既然函数是对象,那么自然满足下面两个条件:

 

1. 其可以被赋给其他变量

 

 

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

def foo():
    print 'foo'
bar=foo
bar()
foo()
print id(foo),id(bar)

执行结果:

foo
foo
60234440 60234440

 

 

2. 其可以被定义在另外一个函数内(作为参数&作为返回值),类似于整型,字符串等对象。

 

函数名作为参数:

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

#*******函数名作为参数**********
def foo(func):
    print 'foo'
    func()

def bar():
    print 'bar'

foo(bar)

执行流程:

1.定义foo()和bar()
2.调用foo(bar)
3.foo(bar)的流程:先执行print 'foo',接着调用func(),即将调用bar()的结果作为参数传递到foo()函数中;而调用bar即执行print 'bar'。
所以,执行func()即print 'bar'.
最终结果是:
先打印出foo,再打印出bar

 

 

 

执行结果:

foo
bar

 

函数名作为返回值

 

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

#*******函数名作为返回值*********
def foo():
    print 'foo'
    return bar

def bar():
    print 'bar'

b=foo()
b()

 

执行流程:

1.先定义foo()和bar()
2.b=foo()的结果:把调用foo()的print内容赋值给b,此时会打印出foo
3.b():执行return bar,即调用bar()函数
最终结果是:
foo
bar

 

 

执行结果:

foo
bar

 

注意:这里说的函数都是指函数名,比如foo;而foo()已经执行函数了,foo()是什么类型取决于return的内容是什么类型!!!

 另外,如果大家理解不了对象,那么就将函数理解成变量,因为函数对象总会由一个或多个变量引用,比如foo,bar。

 

 

函数的嵌套和闭包

 

 抛一个小问题:bar()是什么?

 

def foo():
    print('foo')
    def bar():
        print('bar')
    # bar()
bar()

 

是的,bar就是一个变量名,有自己的作用域的。

  Python允许创建嵌套函数。通过在函数内部def的关键字再声明一个函数即为嵌套:

闭包:如果在一个内部函数里,对在外部作用域(但不是全局作用域)的变量进行引用,那么内部函数就认为是闭包。      

闭包的意义:返回的函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域

 

 

 

def ounter():
    name = 'duart'
 
    def inner():     #闭包条件一 inner就是内部函数
        print("在inner里打印外层函数的变量",name)       #闭包条件二,外部环境的一个变量
 
    return inner                                #结论,内部函数inner就是一个闭包
 
f = ounter()
print(f())
 
# 结果:
# 在inner里打印外层函数的变量 duart
# None    

 

 

 

装饰器

 

 

 

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

 

装饰器概念

1. 究其核心而言,装饰器就是一个可以接受调用也可以返回调用的调用。装饰器无非就是一个函数,该函数接受被装饰的函数作为其位置参数。

2.装饰器通过使用该参数来执行某些操作,然后返回原始参数或一些其他的调用

1、装饰器是在函数调用之上的修饰。这些修饰仅是当声明一个函数或者方法的时候,才会应用的额外调用
2、装饰器的语法以@开头,接着是装饰器函数的名字和可选的参数。紧跟着装饰器声明的是被修饰的函数,和装饰函数的可选参数
3、装饰器的应用是通过在装饰器前放置一个@字符,装饰器看起来会是这样 :

@deco(dec_opt_args)
    def func(func_opt_args): 

 

 

4、装饰器还可以堆叠,实现多装饰器

@deco2
@deco1
def func(arg1, arg2, ...): pass 

 

这等同于:

def func(arg1, arg2, ...): pass
func = deco2(deco1(func)) 

 它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器。

没有参数的装饰器:

@deco
def foo(): pass 

 

等同于:

foo = deco(foo) 

 

带参数的装饰器

@decomaker(deco_args)
def foo(): pass 

 

要自己返回以函数作为参数的装饰器。换句话说,decomaker()用 deco_args 做了些事并返回函数对象,而该函数对象正是以 foo 作为其参数的装饰器。

简单的说来:

foo = decomaker(deco_args)(foo) 

 

多装饰器例子:

@deco1(deco_arg)
@deco2
def func(): pass 

等同于:

func = deco1(deco_arg)(deco2(func)) 

 

装饰器的应用是自底往上的,这与函数的解析(由内向外)是相同的。

 

简单装饰器

 

业务生产中大量调用的函数

 

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

def foo():
    print 'hello foo'
foo()

 

 

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

 

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

import time
def foo():
    start_time=time.time()
    print 'hello foo'
    time.sleep(3)
    end_time=time.time()
    print 'spend %s'% (end_time-start_time)

foo()

 

执行结果:

hello foo
spend 3.00100016594

 

bar()、bar2()也有类似的需求,怎么做?再在bar函数里调用时间函数?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,

重新定义一个函数:专门设定时间:

 

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

import time

def show_time(func):
    start_time=time.time()
    func()
    end_time=time.time()
    print 'spend %s'% (end_time-start_time)

def foo():
    print 'hello foo'
    time.sleep(3)

show_time(foo)

 

执行流程:

1.函数执行都是从上往下执行的,所以先定义show_time(func)和foo()
2.调用show_time(foo):
执行 start_time=time.time(),执行func(),而执行 func()本质上是调用foo();
执行end_time=time.time()和print 'spend %s'% (end_time-start_time)
3.最终结果是:
hello foo
spend 21.2850000858

 

执行结果:

hello foo
spend 21.2850000858

 

逻辑上不难理解,而且运行正常。 但是这样的话,你基础平台的函数修改了名字,容易被业务线的人投诉的,因为我们每次都要将一个函数作为参数传递给show_time函数。而且这种方式已经破坏了原有的代码逻辑结构,之前执行业务逻辑时,执行运行foo(),但是现在不得不改成show_time(foo)。那么有没有更好的方式的呢?当然有,答案就是装饰器。

 

如果能够做到:foo()==show_time(foo) :问题就解决了! 

 即执行foo()能够达到show_time(foo)的效果

 

所以,我们需要show_time(foo)返回一个函数对象,而这个函数对象内则是核心业务函数:执行func()与装饰函数时间计算,修改如下:

 

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

import time

def show_time(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print 'spend %s'%(end_time-start_time)

    return wrapper

def foo():
    print "hello foo"
    time.sleep(3)

foo = show_time(foo)

foo()

 

执行流程:

1.函数执行都是从上往下执行的,所以先定义show_time(func)和foo()
2.foo = show_time(foo):把调用show_time(foo)的结果赋值给foo:本质上就是就是定义 wrapper()和执行return wrapper
3.调用foo():先调用wrapper(),即执行start_time = time.time()、执行func()、执行end_time = time.time()和
print 'spend %s'%(end_time-start_time);而执行func()的本质是调用foo():即执行print "hello foo"和time.sleep(3)

所以最后调用的顺序是:
1.start_time = time.time()
2.print "hello foo"
3.time.sleep(3)
4.end_time = time.time()
5.print 'spend %s'%(end_time-start_time)

 

 

执行结果:

hello foo
spend 3.00100016594

 

函数show_time就是装饰器,它把真正的业务方法func包裹在函数里面,看起来像foo被上下时间函数装饰了。在这个例子中,函数进入和退出时 ,被称为一个横切面(Aspect),这种编程方式被称为面向切面的编程(Aspect-Oriented Programming)。

 

@符号是装饰器的语法糖,在定义函数的时候使用,避免再一次赋值操作

 

下面看一个例子:

 

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

import time

def show_time(func):
    def wrapper():
        start_time = time.time()
        func()
        end_time = time.time()
        print 'spend %s'%(end_time-start_time)

    return wrapper

@show_time  #等同于foo=show_time(foo)
def foo():
    print "hello foo"
    time.sleep(3)


@show_time  #等同于bar=show_time(bar)
def bar():
    print('in the bar')
    time.sleep(2)



foo()
print '***********'
bar()

 

执行流程:

1.函数执行都是从上往下执行的,所以先定义show_time(func)
2.执行@show_time,也等同于执行foo=show_time(foo),也就是定义show_time(func)和 wrapper()以及return wrapper
3.调用foo(),本质上调用show_time(func)下的wrapper(),此时会执行start_time = time.time()、 func()、end_time = time.time()
print 'spend %s'%(end_time-start_time);而执行func()本质上是调用foo():此时会执行 print "hello foo" 和time.sleep(3)
4.执行print '***********'
5.调用bar(),调用bar()和调用foo()都是一样的执行流程,就不细说了

 

 

执行结果:

hello foo
sp2.end 3.00099992752
***********
in the bar
spend 2.0

 

如上所示,这样我们就可以省去bar = show_time(bar)这一句了,直接调用bar()即可得到想要的结果。如果我们有其他的类似函数,我们可以继续调用装饰器来修饰函数,而不用重复修改函数或者增加新的封装。这样,我们就提高了程序的可重复利用性,并增加了程序的可读性。

 

这里需要注意的问题:  foo=show_time(foo)其实是把wrapper引用的对象引用给了foo,而wrapper里的变量func之所以可以用,就是因为wrapper是一个闭包函数。

 

这个要留意一下:

 

 

 

@show_time帮我们做的事情就是当我们执行业务逻辑foo()时,执行的代码由粉框部分转到蓝框部分,仅此而已!

 

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

 

 

单个装饰器的使用:装饰器的执行原理

 

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

#1.定义两个装饰器
def makeBold(fn):
    print "makeBold"

    def wrapper():
        print "hello makeBold"

    return wrapper

def makeItalic(fn):
    print "makeItalic"

    def wrapper():
        print "hello makeItalic"
return wrapper
#2.装饰器的使用,直接@加上函数名的形式,放到需要装饰的函数头上即可 #效果等同于test_Bold=makeBold(test_Bold),装饰器放在一个函数上,相当于将这个函数当成参数传递给装饰函数 @makeBold def test_Bold(): print "test_Bold" return "this is test_Bold" #效果等同于test_Italic=makeItalic(test_Italic),装饰器放在一个函数上,相当于将这个函数当成参数传递给装饰函数 @makeItalic def test_Italic(): print "test_Italic" return "this is test_Italic"

 

执行结果:

makeBold
makeItalic

 

原因分析:

直接执行python程序,不调用任何方法,发现makeBold与makeItalic函数里面的第一个print函数也执行了。
因为@makeBold其效果等同于test_Bold=makeBold(test_Bold),所以程序执行时,从上到下加载执行到函数体上方的标识符@makeBold@makeItalic时,
相当于执行了test_Bold=makeBold(test_Bold),test_Italic=makeBold(test_Italic),所以相当于调用了makeBold和makeItlic这两个函数,
所以依次执行了这两个函数中第一个print语句。
因为@makeBold在前,所以结果就是makeBold 和makeItalic(其实这两个函数还有返回值,返回值分别是这两个函数内部的闭包,只是这里没有显示出来而已)

 

 

总结要点1:

python中装饰器是随着程序的加载运行而自动加载的,跟调不调用方法没有关系.所以只要是装饰器内部函数以外的部分都会自动加载执行,不用调用。

 

 

 带参数的被装饰函数

 

例子1:先把结果打印出来,再显示花费的时间

 

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

import time

def show_time(func):

    def wrapper(a,b):
        start_time = time.time()
        func(a,b)
        end_time = time.time()
        print 'spend %s'%(end_time-start_time)

    return wrapper

@show_time  #等同于add=show_time(add)
def add(a,b):
    time.sleep(1)
    print a+b

add(2,4)

 

 

执行流程:

1.函数执行都是从上往下执行的,所以先定义show_time(func)
2.执行@show_time,也等同于执行foo=show_time(foo),也就是定义show_time(func)和 wrapper()以及return wrapper
3.调用add(2,4),本质上调用show_time(func)下的wrapper(a,b),此时会执行start_time = time.time()、 func(a,b)、end_time = time.time()
print 'spend %s'%(end_time-start_time);而执行func(a,b)本质上是调用add(a,b):此时会执行time.sleep(1) 和print a+b

 

执行结果:

6
spend 1.00099992752

 

 

 

例子2:先显示花费的时间,再把结果打印出来

 

#!/usr/bin/env python
#coding:utf-8
import time

def show_time(func):

    def wrapper(a,b):
        start_time=time.time()
        ret=func(a,b)
        end_time=time.time()
        print('spend %s'%(end_time-start_time))
        return ret

    return wrapper

@show_time   #add=show_time(add)
def add(a,b):

    time.sleep(1)
    return a+b

print add(2,5)

 

 

执行流程:

1.函数执行都是从上往下执行的,所以先定义show_time(func)
2.执行@show_time,也等同于执行foo=show_time(foo),也就是定义show_time(func)和 wrapper()以及return wrapper
3.调用add(2,5),本质上调用show_time(func)下的wrapper(a,b),此时会执行start_time = time.time()、 ret=func(a,b)、end_time = time.time()
print 'spend %s'%(end_time-start_time);而执行ret=func(a,b)本质上是调用add(a,b):此时会执行time.sleep(1) 和return a+b

 

 

执行结果:

spend 1.0
7

 

 

 调用被装饰器装饰后的函数

 

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

#1.定义两个装饰器
def makeBold(fn):
    print "makeBold"

    def wrapper():
        print "hello makeBold"
        return fn()
    return wrapper

def makeItalic(fn):
    print "makeItalic"


    def wrapper():
        print "hello makeItalic"
        return fn
    return wrapper

#2.装饰器的使用,直接@加上函数名的形式,放到需要装饰的函数头上即可

#效果等同于test_Bold=makeBold(test_Bold),装饰器放在一个函数上,相当于将这个函数当成参数传递给装饰函数
@makeBold
def test_Bold():
    print "test_Bold"
    return "this is test_Bold"


#效果等同于test_Italic=makeItalic(test_Italic),装饰器放在一个函数上,相当于将这个函数当成参数传递给装饰函数
@makeItalic
def test_Italic():
    print "test_Italic"
    return "this is test_Italic"

#-----------下面对函数进行调用-----------------
t = test_Bold()   #调用test_Bold()函数,相当于:makeBold(test_Bold)()
print t  #打印test_Bold函数返回值

 

原因分析:

 1.首先,程序执行后,从上到下加载,肯定是先加载到@makeBold@makeItalic时,原理同上,这时候先把makeBold和makeItalic打印出来了。
 2.因为@makeBold其效果等同于test_Bold=makeBold(test_Bold),所以这个时候程序在打印完makeItalic以后,执行返回语句:return wrapper ;
  因为wrapper是个函数引用, 所以这个时候结果相当于test_Bold=wrapper。即test_Bold指向闭包函数wrapper。这个时候程序运行t = test_Bold()执行时,
等同于执行了t=test_Bold()=wrapper()函数。所以这个时候执行了wrapper函数。先执行了print "hello makeBold"
(同理虽然@makeItalic装饰的test_Italic函数的返回值也是对应的wrapper函数引用,但是因为后续没有调用wrapper函数,所以wrapper的函数内部没有执行。
  这里是难点,难点,难点)
3.接着往下执行wrapper函数的return语句,因为return语句里有调用了函数test_Bold()。所以这个时候去执行test_Bold()函数,
  所以执行了该函数内的
print "test_Bold"语句
4.这个时候test_Bold()执行return语句,返回值是this
is the test_Bold,在wrapper函数的return fn() 中作为参数使用。
5.所以wrapper的函数的返回值是:
this is the test_Bold,最后将这个返回值,赋给t,并且打印了t。所以整个函数调用语句的结果就是如上。

 

总结要点2:

     1.装饰器是随着程序的执行而加载的,不是调用函数也会自动加载。

     2.装饰器原理:@装饰器名(@makeBold) 放在一个函数头上相当于将这个函数整体当做参数传递给这个装饰函数去执行,
    即等价于test_Bold=makeBold(test_Bold),装饰器的使用大大简化了程序的代码。

 

 

不定长参数

 

 

 

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

#***********************************不定长参数
import time

def show_time(func):

    def wrapper(*args,**kwargs):
        start_time=time.time()
        func(*args,**kwargs)
        end_time=time.time()
        print('spend %s'%(end_time-start_time))

    return wrapper

@show_time   #add=show_time(add)
def add(*args,**kwargs):

    time.sleep(1)
    sum=0
    for i in args:
        sum+=i
    print(sum)

add(2,4,8,9)

 

 

 

 

 

执行结果:

23
spend 1.0

 

 

带参数的装饰器

 装饰器还有更大的灵活性,例如带参数的装饰器:在上面的装饰器调用中,比如@show_time,该装饰器唯一的参数就是执行业务的函数。装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。

 

例子:

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

import time

def time_logger(flag=0):
    def show_time(func):
        def wrapper(*args,**kwargs):
            start_time = time.time()
            func(*args,**kwargs)
            end_time=time.time()
            print 'spend %s'%(end_time-start_time)

            if flag:
                print '将这个操作的时间记录到日志中'
        return wrapper
    return show_time

@time_logger(3)
def add(*args,**kwargs):
    time.sleep(1)
    sum100=0
    for i in args:
        sum100 += i
    print sum100

add(2,7,5)

 

执行流程:

1.函数执行都是从上往下执行的,所以先定义time_logger(flag=0)
2.执行@time_logger(3),本质上是定义show_time(func)和return show_time
3.执行完return show_time后马上定义wrapper(*args,**kwargs)和return wrapper
4.调用 add(2,7,5),本质上是调用 wrapper(*args,**kwargs);
此时会执行:start_time = time.time()、func(*args,**kwargs)、end_time=time.time()和print 'spend %s'%(end_time-start_time) 以及: if flag: print '将这个操作的时间记录到日志中'
而执行func(*args,**kwargs)本质上调用add(*args,**kwargs):



 

 

执行结果:

14
spend 1.00099992752
将这个操作的时间记录到日志中

 

@time_logger(3) 做了两件事:

 

1.time_logger(3):得到闭包函数show_time,里面保存环境变量flag

2.@show_time   :add=show_time(add)

 

 

 

上面的time_logger是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器(一个含有参数的闭包函数)。当我 们使用@time_logger(3)调用的时候,Python能够发现这一层的封装,并把参数传递到装饰器的环境中。

 

 

多层装饰器

 

多个装饰器的执行顺序 :

装饰器函数的执行顺序是分为(被装饰函数)定义阶段和被装饰函数)执行阶段的,装饰器函数在被装饰函数定义好后立即执行

    在函数定义阶段:执行顺序是从最靠近函数的装饰器开始,自内而外的执行
    在函数执行阶段:执行顺序由外而内,一层层执行

 

例子:

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


import time

def test1(func):
    def wrapper():
        print "test1 starting"
        func()
        print "test1 ended"

    return wrapper


def test2(func):
    def wrapper():
        print "test2 starting"
        func()
        print "test2 ended"
    return wrapper



@test1
@test2
def foo():
    print "hello foo"
    time.sleep(3)

foo()

 

执行流程:

1.函数执行都是从上往下执行的,所以先定义test1(func)和test2()
2.顺序执行@test1和@test2
3.当执行到装饰器@test2时,就定义test2(func)下的 wrapper()和执行return wrapper
4.当执行装饰器@test1时,就定义test1(func)下的 wrapper()和执行return wrapper
5.调用foo():此时会先调用test1(func)下的 wrapper(),
即:print "test1 starting",func()以及后面的print 'test1 ended'都不会执行,即不会显示内容;
然后调用test2(func)下的 wrapper():
即:print "test2 starting",func()以及后面的print 'test2 ended'都不会执行,即不会显示内容;
6.调用装饰器,此时会执行foo()函数的内容,即执行print 'hello foo'和time.sleep(3)
7.此时因为装饰器@test2靠近被装饰的函数foo,所以会先执行print "test2 ended";然后执行print "test1 ended"

 

 

执行结果:

test1 starting
test2 starting
hello foo
test2 ended
test1 ended

 

 多装饰器的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器

 

下面再看一个例子:

 python中多个装饰器的调用顺序详解

前言

一般情况下,在函数中可以使用一个装饰器,但是有时也会有两个或两个以上的装饰器。多个装饰器装饰的顺序是从里到外(就近原则),而调用的顺序是从外到里(就远原则)。

原代码

 

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


def f1(fn):
    print "f1"

    def inner(*args,**kwargs):
        print "进入f1"
        result = fn(*args,**kwargs)
        print "进入f1里面"
    return inner

def f2(fn):
    print "f2"

    def inner(*args,**kwargs):
        print "进入f2"
        result = fn(*args,**kwargs)
        print "进入f2里面"
    return inner
@f1
@f2
def fn():
    print "fn self"

fn()


 

执行结果:

f2
f1
进入f1
进入f2
fn self
进入f2里面
进入f1里面

 

先理解一下概念:

装饰顺序 : 就近原则

被装饰的函数,组装装饰器时,是从下往上装饰

执行顺序 : 就远原则

装饰器调用时是从上往下调用

 

由这个例子可知:

先分析装饰顺序:f1(f2(fn))

1.代码从上往下执行,先定义f1(fn)函数,再定义f2(fn)函数,接着遇到@f1和@f2装饰器,并且这两个装饰器装饰的是fn函数

2.根据装饰顺序可知:fn是被装饰的函数,首先用@f2装饰,然后用@f1装饰

3.当执行到@f2时,就会跳转到f2函数中;此时会执行print f2,即打印出f2,并且返回一个函数。此时f2(fn)执行完成。

4.然后因为装饰顺序的关系,f2函数执行完后,接下来就会跳转到f1函数中;此时会执行print f1,即打印出f1,并且把f2返回的函数传递给f1,即执行f1(f2(fn))。

 

此时装饰顺序的结果自然显而易见:先打印出f2,再打印出f1

 

f2
f1

 

至此装饰顺序分析完毕,即f1(f2(fn))执行完毕

 

接着分析执行顺序:f()

1.装饰顺序分析完毕后,就执行 fn():即函数执行

2.根据执行顺序可知:装饰器调用时是从上往下调用。因此先执行f1(fn)函数的内嵌函数

3.在f1(fn)函数的内嵌函数中,先执行print "进入f1",然后执行result = fn(*args,**kwargs),此时因为f1函数是作为参数传递给f2函数的,所以会跳转到f2函数的内嵌函数中执行

4.这时就会执行print "进入f2",然后执行result = fn(*args,**kwargs),此时会把返回值传递给fn,随即执行print "fn self"

5.执行完以后,就执行f2函数中的 print "进入f2里面",然后再执行f1函数中的 print "进入f2里面",至此整个多层装饰器调用完毕

 

总结:

装饰顺序 : 就近原则

被装饰的函数,组装装饰器时,是从下往上装饰

执行顺序 : 就远原则

装饰器调用时是从上往下调用

 

为了更好的理解,找到这段话:

被装饰的函数是一个妹子,装饰器是衣服。“办事情”的时候得依次把外套、衬衣、内衣脱掉,事情办完了还要依次把内衣、衬衣、外套穿上。距离“妹子”越近的装饰器代表越贴身的衣服。

 

 

 

 

 

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

def war1(func):
    print "war1"

    def inner(*args,**kwargs):
        print "----war1 start----"
        func(*args,**kwargs)
        print "---war1 end --- "

    return inner

def war2(func):
    print "war2"
    def inner(*args,**kwargs):
        print "======war2 start====="
        func(*args,**kwargs)
        print "======war2 end====="
    return inner

@war1
@war2
def f():
    print "--- self ---"
    
f()

 

 

执行结果:

 

war2
war1
----war1 start----
======war2 start=====
--- self ---
======war2 end=====
---war1 end --- 

 

 

那么为什么会是这样的顺序

看到这个执行结果,我们可能会疑惑,为什么先打印了war2 和war1呢?

首先要知道,装饰器函数在被装饰函数定义好后就立即执行了

我们去掉函数执行,只留下函数的定义,代码如下:

 

 

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

def war1(func):
    print "war1"

    def inner(*args,**kwargs):
        print "----war1 start----"
        func(*args,**kwargs)
        print "---war1 end --- "

    return inner

def war2(func):
    print "war2"
    def inner(*args,**kwargs):
        print "======war2 start====="
        func(*args,**kwargs)
        print "======war2 end====="
    return inner

@war1
@war2
def f():
    print "--- self ---"

 

 

执行结果:

war2
war1

 

 

也就是说在 f 方法没有执行的时候,装饰器函数就执行了

此处我们需要先弄清楚,函数和函数调用的区别,f 是一个函数,它的值是函数本身, f( )是函数的调用,它的值是函数的执行结果

在被装饰函数定义阶段,也就是函数调用之前:

 

@war1
@war2
def f():
    print "--- self ---"

 

这段代码相当于:

war1(war2(f))

 

而函数调用之前的结果:先打印war2,再打印war1

 

 

war1和war2的返回值都是一个函数,所以war1(war2(f))也是一个函数,war2包含了f函数,war1包含了war2

 f()  相当于  war1(war2(f))()  

 

所以f ( )在执行时,war2-->war1--> war1-->war2-->f -->war2-->war1 按照这样的顺序执行

 

 

 多个装饰器同时修饰函数:装饰器执行顺序

 

 

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

#1.定义两个装饰器
def makeBold(fn):
    print "makeBold"

    def wrapped1():  #注意为了演示结果这里讲wrapped函数,分为wrapped1,wrapped2
        print "hello makeBold"
        return fn()
    return wrapped1

def makeItalic(fn):
    print "makeItalic"


    def wrapped2():  #注意为了演示结果这里讲wrapped函数,分为wrapped1,wrapped2
        print "hello makeItalic"
        return fn()
    return wrapped2

#2.使用两个装饰器同时装饰一个函数,可以三个,甚至多个。原理一样
@makeBold   #注意2.其效果等同于test_B_I=makeBold( makeItalic(test_B_I) )
@makeItalic #注意1.其效果等同于test_B_I=makeItalic(test_B_I)
def test_B_I():   
    print("test_B_I"*5)
    return "this is the test_B_I"

 

 

结果:注意makeItalic和makeBold的顺序:

 

makeItalic
makeBold

 

原因分析:

1.大家注意了,虽然@makeBold 写在了@makeItalic的上面,但是结果显示,很明显先执行的是@makeItalic,即makeItalic函数时先加载执行的。
所以当一个函数被多个装饰器装饰时,装饰器的加载顺序是从内到外的。其实很好理解:装饰器是给函数装饰的,所以要从靠近函数的装饰器开始从内往外加载。
所以:打印的结果是makeItalicmakeBold

 

注意:

@makeBold  #注意2:代码中的@makeBold其效果test_B_I = makeBold(makeItalic(test_B_I)) ,即对下面makeItalic装饰后的结果进行装饰
@makeItalic #注意1::其效果等同于test_B_I= makeItalic(test_B_I)
def test_B_I():
    print("test_B_I"*5)
    return "this is the test_B_I"

 

 

调用被两个装饰器装饰后的函数

 

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

#1.定义两个装饰器
def makeBold(fn):
    print "makeBold"

    def wrapped1():  #注意为了演示结果这里讲wrapped函数,分为wrapped1,wrapped2
        print "hello makeBold"
        return fn()
    return wrapped1

def makeItalic(fn):
    print "makeItalic"


    def wrapped2():  #注意为了演示结果这里讲wrapped函数,分为wrapped1,wrapped2
        print "hello makeItalic"
        return fn()
    return wrapped2

#2.使用两个装饰器同时装饰一个函数,可以三个,甚至多个。原理一样
@makeBold   #注意2.其效果等同于test_B_I=makeBold( makeItalic(test_B_I) )
@makeItalic #注意1.其效果等同于test_B_I=makeItalic(test_B_I)
def test_B_I():
    print "test_B_I"*5
    return "this is the test_B_I"

#-----注意下面对被两个装饰器修饰的函数进行调用--------------------
test_B_I()  #调用被两个装饰器修饰后的函数test_B_I()
print test_B_I() #打印test_B_I的返回值

 

执行结果:

makeItalic
makeBold
hello makeBold
hello makeItalic
test_B_Itest_B_Itest_B_Itest_B_Itest_B_I
hello makeBold
hello makeItalic
test_B_Itest_B_Itest_B_Itest_B_Itest_B_I
this is the test_B_I

 

原因分析:

1.同理上面,因为装饰器是给函数装饰的,所以当一个函数被多个装饰器装饰时,装饰器的加载顺序是从内到外的,从下往上的,所以:打印的结果是makeItalic和makeBold

#2.使用两个装饰器同时装饰一个函数,可以三个,甚至多个。原理一样
@makeBold #注意2.其效果等同于test_B_I=makeBold( makeItalic(test_B_I) )
@makeItalic #注意1.其效果等同于test_B_I=makeItalic(test_B_I)
def test_B_I():
print "test_B_I"*5
return "this is the test_B_I"

3.注意上面代码,多个装饰器时,装饰器是从内往外加载。所以先是@makeItalic装饰test_B_I函数,其效果等同于test_B_I= makeItalic(test_B_I)。
返回值是其内部的wrapped2函数引用。@makeItalic加载执行后的结果是:test_B_I=wrapped2。这个时候@makeBold装饰器再对这个结果(函数)进行装饰。
所以这时候@makeBold装饰效果等价于: test_B_I  = makeBold(makeItalic(test_B_I)),也等价于test_B_I=makeBold(wrapped2).
又因为makeBold()的返回值是wrapped1。即makeBold(wrapped2)= wrapped1。所以最后@makeBold@makeItalic装饰后的结果时test_B_I=wrapped1。
只是在wrapped1里面调用了wrapped2。所以,当执行函数调用语句test_B_I()时,相当于执行了wrapped1().这个时候打印了wrapped1函数内部的print "hello makeBold",
所以接着结果是:hello makeBold

4.紧接着,执行wrapped1函数的return语句:return fn() 。因为@makeBold装饰的效果等价于makeBold(makeItalic(test_B_I)),即makeBold装饰的函数是@makeItalic装饰后的结果。
等价于makeBold(wrapped2)。所以实际执行的是:return +wrapped2()。故这个时候要调用wrapped2函数。执行了print "hello makeItalic"
所以接着打印了结果是:hello makeItalic

5.接着执行wrapped2里面的return fn()"。因为wrapped2装饰的是test_B_I函数,所以这里fn()=test_B_I().这个时候又去调用test_B_I()函数,
所以执行了print("test_B_I"*5)。打印了:test_B_Itest_B_Itest_B_Itest_B_Itest_B_I

6.因为test_B_I的返回值是this is the test_B_I。所以wrapped2的返回值是this is the test_B_I。所以wrapped1的返回值是:this is the test_B_I。
所以最后被两个装饰器装饰后的test_B_I()函数的返回值结果是:this is the test_B_I
所以最后整个函数调用的结果如上

 

关于多个装饰器修饰一个函数总结要点:

1.当一个函数被多个装饰器装饰时,装饰器的加载顺序是从内到外的(从下往上的)。其实很好理解:装饰器是给函数装饰的,所以要从靠近函数的装饰器开始从内往外加载
2.外层的装饰器,是给里层装饰器装饰后的结果进行装饰。相当于外层的装饰器装饰的函数是里层装饰器的装饰原函数后的结果函数(装饰后的返回值函数)。

 

 

 

类装饰器

 

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

 

import time

class Foo(object):
    def __init__(self, func):
        self._func = func

    def __call__(self):
        start_time=time.time()
        self._func()
        end_time=time.time()
        print('spend %s'%(end_time-start_time))

@Foo  #bar=Foo(bar)

def bar():

    print ('bar')
    time.sleep(2)

bar()    #bar=Foo(bar)()>>>>>>>没有嵌套关系了,直接active Foo的 __call__方法

 

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring、__name__、参数列表,先看例子:

def foo():
    print("hello foo")

print(foo.__name__)
#####################

def logged(func):
    def wrapper(*args, **kwargs):

        print (func.__name__ + " was called")
        return func(*args, **kwargs)

    return wrapper


@logged
def cal(x):
   return x + x * x


print(cal.__name__)

########
# foo
# wrapper

 

解释:

@logged
def f(x):
   return x + x * x

等价于:

ef f(x):
    return x + x * x
f = logged(f)
不难发现,函数f被wrapper取代了,当然它的docstring,__name__就是变成了wrapper函数的信息了。

print f.__name__    # prints 'wrapper'
print f.__doc__     # prints None
这个问题就比较严重的,好在我们有functools.wraps,wraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器函数中,
这使得装饰器函数也有和原函数一样的元信息了。
from functools import wraps
 
 
def logged(func):
 
    @wraps(func)
 
    def wrapper(*args, **kwargs):
        print (func.__name__ + " was called")
        return func(*args, **kwargs)
    return wrapper
 
@logged
def cal(x):
   return x + x * x
 
print(cal.__name__)  #cal

 

 

1.什么是装饰器

  • 装饰器其实就是一个闭包,把一个函数当做参数然后返回一个替代版函数;

  • 装饰器的功能在于对函数或类功能的增强,这是一种低耦合的功能增强;

2.装饰器特点

  • 开放封闭原则,即对扩展是开放的,对修改时封闭的;

  • 装饰器本质可以是任意可调用的对象,被装饰的对象也可以是任意可调用对象;

  • 装饰器的功能是在不修改被装饰器对象源代码以及被装饰器对象的调用方式的前提下为其扩展新功能;

  • 装饰器本质是函数,(即装饰其他函数)就是为其他函数添加附加功能;

3.无参装饰器

  • 就是不带参数的装饰器,是最简单的装饰器,返回包裹函数;

  • *args表示的参数以列表的形式传入;

  • **kwargs表示的参数以字典的形式传入;

  • @decorator这个语法相当于 执行 func = decorator(func),为func函数装饰并返回,且@修饰符必须出现在函数定义前一行,不允许和函数定义在同 一行;

4.带参装饰器

  • 就是带参数的装饰器,即复杂装饰器,返回包裹函数;

  • 实现一个装饰器,它用来检查被装饰函数的参数类型,装饰器可以通过参数指明函数参数的类型,调用时如果检测出类型不匹配则抛出异常。

5.functools模块

  • 用来拷贝属性;

  • functools,用于高阶函数,指那些作用于函数或者返回其它函数的函数,通常只要是可以被当做函数调用的对象就是这个模块的目标。

 

 

今天学习的是装饰器,主要分为带参和不带参的两种装饰器,装饰器是多样变化的,大家一定要多实践几遍,多熟悉熟悉;

课外补充

为什么去掉装饰器内层函数warpper会报错?

答:decorated这个函数里面,要记得返回f函数哦,装饰器是需要返回传入的函数的,并且反回的不需要加入括号(),加入括号就会报错,也可以理解为加入括号,没有调用久执行了,效果如下图:

wraps函数的作用?

答: 如图所示,在python中,我们使用了装饰器后,定义了增强函数my_dec_example(),然后返回增强的函数用来装饰example()这个函数,然后我们调用example()函数,这个时候实际间接调用了my_dec_example()函数,python会认为这个函数属性如名称等就是my_dec_example()的函数的属性。

但实际上我们调用的是example()这个函数,函数的属性应该都是example()相关的,所以为了解决这样的问题,就使用@functools.wraps(fn)这个函数,它的作用是拷贝属性,也就是把fn函数的属性拷贝带过来,比如:__name__,给到返回的函数my_dec_example(),这样,我们调用example()的时候,example()已有的属性在传给装饰器函数,然后再返回来,就避免私有属性不一致的问题了。

 

 

参考:https://www.cnblogs.com/wj-1314/p/8538716.html

 https://blog.csdn.net/u013411246/article/details/80571462

 

posted @ 2019-06-18 23:05  钟桂耀  阅读(177)  评论(0编辑  收藏  举报