【函数】0五、装饰器由浅入深


原文http://www.cnblogs.com/feixuelove1009/p/5541632.htmlhtml

  装饰器的功能在不少语言中都有,名字也不尽相同,其实它体现的是一种设计模式,强调的是开放封闭原则,更多的用于后期功能升级而不是编写新的代码。装饰器不光能装饰函数,也能装饰其余的对象,好比类,但一般,咱们以装饰函数为例子介绍其用法。要理解在Python中装饰器的原理,须要一步一步来。本文尽可能描述得浅显易懂,从最基础的内容讲起。python


装饰器的做用:增长被装饰器装饰的函数的功能
shell


1、Python的函数相关基础编程

  第一,必须强调的是python是从上往下顺序执行的,并且碰到函数的定义代码块是不会当即执行它的,只有等到该函数被调用时,才会执行其内部的代码块。            设计模式

In [2]: def foo():
   ...:     print("foo函数被运行了")
   ...:     
   # 若是就这么样,foo里的语句是不会被执行的;程序只是简单的将定义代码块读入内存中

再看看,顺序执行的例子:安全

def foo():
    print("我是上面的函数定义!")

def foo():
    print("我是下面的函数定义!")
 
 foo()

运行结果:
 
我是下面的函数定义

  可见,由于顺序执行的缘由,下面的foo将上面的foo覆盖了。所以,在Python中代码的放置位置是有要求的,不能随意摆放,函数体要放在被调用的语句以前。bash

  其次,咱们还要先搞清楚几样东西:函数名函数体返回值函数的内存地址函数名加括号函数名被看成参数函数名加括号被看成参数返回函数名返回函数名加括号。             
app

对于以下的函数:      运维

In [10]: def foo():
    ...:     print("让咱们干点啥!")
    ...:     return "ok"
    ...: 

In [12]: foo
Out[12]: <function __main__.foo>    
        
In [11]: foo()
让咱们干点啥!
Out[11]: 'ok'

函数名: fooide

函数体: 第1-3行

返回值: 字符串“ok” 若是不显示给出return的对象,那么默认返回None

函数的内存地址: 当函数体被读进内存后的保存位置,它由标识符即函数名foo引用,也就是说foo指向的是函数体在内存内的保存位置。

函数名加括号:foo(),函数的调用方法,只有见到这个括号,程序会根据函数名从内存中找到函数体,而后执行它



再看下面这个例子:

In [15]: def outer(func):
    ...:     def inner():
    ...:         print('我是内层函数!')
    ...:     return inner
    ...: 

In [16]: def foo():
    ...:     print('我是原始函数')
    ...:     

In [18]: outer(foo)
Out[18]: <function __main__.outer.<locals>.inner>

In [19]: outer(foo)()
我是内层函数!

In [20]: outer(foo())
我是原始函数
Out[20]: <function __main__.outer.<locals>.inner>

In [21]: outer(foo())()
我是原始函数
我是内层函数!

  在python中,一切都是对象,函数也不例外。所以能够将函数名,甚至函数名加括号进行调用的方式做为另外一个函数的参数

  上面代码中,outer和foo是两个函数,outer(foo)表示将foo函数的函数名当作参数传递给outer函数并执行outer函数;outer(foo())表示将foo函数执行后的返回值当作参数传递给outer函数并执行outer函数,因为foo函数没有指定返回值,实际上它传递给了outer函数一个None。注意其中的差异,有没有括号是关键!

  一样,在outer函数内部,返回了一个inner,它是在outer函数内部定义的一个函数,注意,因为inner后面没有加括号,因此返回的是inner的函数体,实际上也就是inner这个名字,一个简单的引用而已。那么,若是outer函数返回的是inner()呢?如今你应该已经很清楚了,它会先执行inner函数的内容,而后返回个None给outer,outer再把这个None返回给调用它的对象。

   请记住,函数名、函数加括号能够被当作参数传递,也能够被当作返回值return,有没有括号是两个大相径庭的意思!           


2、装饰器的使用场景

  装饰器一般用于在不改变原有函数代码和功能的状况下,为其添加额外的功能。好比在原函数执行前先执行点什么,在执行后执行点什么。

  让咱们经过一个例子来看看,装饰器的使用场景和体现的设计模式。(抱歉的是我设计不出更好的场景,只能引用武大神的案例加以演绎)

  有一个大公司,下属的基础平台部负责内部应用程序及API的开发,有上百个业务部门负责不一样的业务,他们各自调用基础平台部提供的不一样函数处理本身的业务,状况以下:

 # 基础平台部门开发了上百个函数
 def f1():
     print("业务部门1数据接口......")
 
 def f2():
     print("业务部门2数据接口......")
 
 def f3():
     print("业务部门3数据接口......")
  .
  .
  .
  
 def f100():
     print("业务部门100数据接口......")
     
 # 各部门分别调用
 f1()
 f2()
 f3()
 f100()

  因为公司在创业初期,基础平台部开发这些函数时,因为各类缘由,好比时间,好比考虑不周等等,没有为函数调用进行安全认证。如今,平台部主管决定弥补这个缺陷,因而:

第一回,主管叫来了一个运维工程师,工程师跑上跑下逐个部门进行通知,让他们在代码里加上认证功能,然而,当天他被开除了。

第二回:主管又叫来了一个运维工程师,工程师用shell写了个复杂的脚本,勉强实现了功能。但他很快就回去接着作运维了,不会开发的运维不是好运维....

第三回:主管叫来了一个python自动化开发工程师,哥们是这么干的:    只对基础平台的代码进行重构,让N个业务部门无需作任何修改。这哥们很快也被开了,连运维也没得作。

def f1():
    #加入认证程序代码
    print("业务部门1数据接口......")

def f2():
    # 加入认证程序代码
    print("业务部门2数据接口......")

def f3():
    # 加入认证程序代码
    print("业务部门3数据接口......")
.
.
.
    
def f100():
    #加入认证程序代码
    print("业务部门100数据接口......")
    
# 各部门分别调用
f1()
f2()
f3()
f100()

第四回:主管又换了个工程师,他是这么干的:定义个认证函数,原来其余的函数调用它,代码以下框。可是,主管依然不满意,不过这一次他解释了为何。主管说:写代码要遵循开放封闭原则,虽然在这个原则主要是针对面向对象开发,可是也适用于函数式编程,简单来讲,它规定已经实现的功能代码内部不容许被修改,但外部能够被扩展

封闭:已实现的功能代码块;

开放:对扩展开放。

若是将开放封闭原则应用在上述需求中,那么就不容许在函数 f1 、f二、f3......f100的内部进行代码   修改。遗憾的是,工程师没有漂亮的女友,因此很快也被开除了。

def login():
    print("认证成功!")
    
    
def f1():
    login()
    print("业务部门1数据接口......")
def f2():
    login()
    print("业务部门2数据接口......")
def f3():
    login()
    print("业务部门3数据接口......")
def f100():
    login()
    print("业务部门100数据接口......")

# 各部门分别调用
f1()
f2()
f3()
f100()

第五回:已经没有时间让主管找别人来干这活了,他决定亲自上阵,而且打算在函数执行后再增长个日志功能。主管是这么想的:不会装饰器的主管不是好码农!要不为啥我能当主管,你只能被管呢?嘿嘿。他的代码以下:

def outer(func):
    def inner():
        print("认证成功!")
        result = func()
        print("日志添加成功")        
        return result    
    return inner
    
@outer
def f1():
    print("业务部门1数据接口......")
    
@outer
def f2():
    print("业务部门2数据接口......")
    
@outer
def f3():
    print("业务部门3数据接口......")
    
@outer
def f100():
    print("业务部门100数据接口......")
    
# 各部门分别调用
f1()
f2()
f3()
f100()

  对于上述代码,也是仅需对基础平台的代码进行拓展,就能够实如今其余部门调用函数 f1 f2 f3 f100 以前都进行认证操做,在操做结束后保存日志,而且其余业务部门无需他们本身的代码作任何修改,调用方式也不用变。“主管”写完代码后,以为独乐了不如众乐乐,打算显摆一下,因而写了篇博客将过程进行了详细的说明。


3、装饰器的内部原理

下面咱们以f1函数为例进行说明:

In [23]: def outer(func):
    ...:     def inner():
    ...:         print('认证成功!')
    ...:         result = func()
    ...:         print('日志添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [24]: @outer        # f1 = outer(f1) = inner
    ...: def f1():
    ...:     print('业务部门1数据接口...')
    ...:     

In [25]: f1()
认证成功!
业务部门1数据接口...
日志添加成功

运用咱们在第一部分介绍的知识来分析一下上面这段代码:

  1. 程序开始运行,从上往下编译,读到def outer(func):的时候,发现这是个“一等公民”->函数,因而把函数体加载到内存里,而后过。

  2. 读到@outer的时候,程序被@这个语法糖吸引住了,知道这是个装饰器,按规矩要当即执行的,因而程序开始运行@后面那个名字outer所定义的函数。(相信没有人会愚蠢的将@outer写到别的位置,它只能放在被装饰的函数的上方最近处,不要空行。)

  3. 程序返回到outer函数,开始执行装饰器的语法规则,这部分规则是定死的,是python的“法律”,不要问为何。

    规则是:被装饰的函数的名字会被看成参数传递给装饰函数。装饰函数执行它本身内部的代码后,会将它的返回值赋值给被装饰的函数。         

这里面须要注意的是:

  • @outer和@outer()有区别:没有括号时,outer函数依然会被执行,这和传统的用括号才能调用函数不一样,须要特别注意!那么有括号呢?那是装饰器的高级用法了,之后会介绍。

  • 是f1这个函数名(而不是f1()这样被调用后)当作参数传递给装饰函数outer,也就是:func = f1,@outer等于outer(f1),实际上传递了f1的函数体,而不是执行f1后的返回值。

  • outer函数return的是inner这个函数名,而不是inner()这样被调用后的返回值。

  若是你对第一部分函数的基础知识有清晰的了解,那么上面的内容你应该很容易理解。

  4. 程序开始执行outer函数内部的内容,一开始它又碰到了一个函数,很绕是吧?固然,你能够在 inner函数先后安排点别的代码,但它们不是重点,并且有点小麻烦,下面会解释。inner函数定义块被程序观察到后不会马上执行,而是读入内存中(这是潜规则)。

  5. 再往下,碰到return inner,返回值是个函数名,而且这个函数名会被赋值给f1这个被装饰的函数,也就是f1 = inner。根据前面的知识,咱们知道,此时f1函数被新的函数inner覆盖了(其实是f1这个函数名更改为指向inner这个函数名指向的函数体内存地址,f1再也不指向它原来的函数体的内存地址),再日后调用f1的时候将执行inner函数内的代码,而不是先前的函数体。那么先前的函数体去哪了?还记得咱们将f1当作参数传递给func这个形参么?func这个变量保存了老的函数在内存中的地址,经过它就能够执行 老的函数体,你能在inner函数里看到result = func()这句代码,它就是这么干的!

  6.接下来,尚未结束。当业务部门,依然经过f1()的方式调用f1函数时,执行的就再也不是老的f1函数的代码,而是inner函数的代码。在本例中,它首先会打印个“认证成功”的提示,很显然你能够换成任意的代码,这只是个示例;而后,它会执行func函数并将返回值赋值个变量result,这个func函数就是老的f1函数;接着,它又打印了“日志保存”的提示,这也只是个示例,能够换成任何你想要的;最后返回result这个变量。咱们在业务部门的代码上能够用 r = f1()的方式接受result的值。

  7.以上流程走完后,你应该看出来了,在没有对业务部门的代码和接口调用方式作任何修改的同时,也没有对基础平台部原有的代码作内部修改,仅仅是添加了一个装饰函数,就实现了咱们的需求,在函数调用前先认证,调用后写入日志。这就是装饰器的最大做用。


问题:那么为何咱们要搞一个outer函数一个inner函数这么复杂呢?一层函数不行吗?  

答:请注意,@outer这句代码在程序执行到这里的时候就会自动执行outer函数内部的代码,若是不封装一下,在业务部门还未进行调用的时候,就执行了些什么,这和初衷有点不符。固然,若是你对这个有需求也不是不行。请看下面的例子,它只有一层函数。  

In [33]: def outer(func):
    ...:     print('认证成功!')
    ...:     result = func()
    ...:     print('日志添加成功')
    ...:     return result
    ...: 

In [34]: @outer
    ...: def f1():
    ...:     print('业务部门1数据接口...')
    ...:     
认证成功!
业务部门1数据接口...
日志添加成功

# 业务部门并无执行f1函数

  看到没?我只是定义好了函数,业务部门尚未调用f1函数呢,程序就把工做全作了。这就是封装一层函数的缘由。


3、装饰器的参数传递

一、被装饰函数的参数传递

  细心的朋友可能已经发现了,上面的例子中,f1函数没有参数,在实际状况中确定会须要参数的,那参数怎么传递的呢?

一个参数的状况:

In [13]: def outer(func):
    ...:     def inner(username):
    ...:         print('认证成功')
    ...:         result = func(username)
    ...:         print('日志添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [14]: @outer
    ...: def f1(name):
    ...:     print("{}正在链接业务部门1数据接口。。。".format(name))
    ...:     

In [15]: f1("jack")
认证成功
jack正在链接业务部门1数据接口。。。
日志添加成功

  在inner函数的定义部分也加上一个参数,调用func函数的时候传递这个参数,很好理解吧?可问题又来了,那么另一个部门调用的f2有2个参数呢?f3有3个参数呢?你怎么传递?

很简单,咱们有*args和**kwargs嘛!号称“万能参数”!

简单修改一下上面的代码:

In [17]: def outer(func):
    ...:     def inner(*args, **kwargs):
    ...:         print('认证成功')
    ...:         result = func(*args, **kwargs)
    ...:         print('日志添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [18]: @outer
    ...: def f1(name, age):
    ...:     print("{}正在链接业务部门1数据接口。。。".format(name))
    ...:     

In [19]: f1("jack", 18)
认证成功
jack正在链接业务部门1数据接口。。。
日志添加成功


二、装饰器本身的参数

  装饰器本身的参数?装饰器本身的参数不是被装饰的函数吗?

     那装饰器如何传入本身的参数呢?

In [31]: # 认证函数

In [32]: def auth(request, kwargs):
    ...:     print("认证成功!")
    ...:     

In [33]: # 日志函数

In [34]: def log(request, kwargs):
    ...:     print("日志添加成功!")
    ...:     

In [35]: # 装饰器函数,接收2个参数,这两个参数应该是某个函数的名字。

In [41]: def Filter(auth_func, log_func):
    ...:     # 第一层封装,auth和log函数的参数值被传递到了这里
    ...:     def outer(main_func):
    ...:         # 第二层封装,f1函数实际上被传递给了main_func这个参数
    ...:         def wrapper(request, kargs): 
    ...:         # 下面代码的判断逻辑不重要,重要的是参数的引用和返回值
    ...:             before_result = auth(request, kargs) 
    ...:             if(before_result  None):
    ...:                 return before_result
    ...:             main_result = main_func(request, kargs)
    ...:             if(main_result != None):
    ...:                 return main_result
    ...:             after_result = log(request, kargs)
    ...:             if(after_result != None):
    ...:                 return after_result
    ...:         return wrapper
    ...:     return outer
    ...: 
    # 注意了,这里的装饰器函数有参数哦,它的意思是先执行Filter函数
    # 而后将Filter函数的返回值做为装饰器函数的名字返回到这里,因此,
    # 其实这里等价于,Filter(auth,log) = outer , @Filter(auth,log) =  @outer
        #  也等价于f1=Filter(auth, log)(f1)=@outer=wrapper
    
In [44]: @Filter(auth, log)  
    ...: def f1(name, age):
    ...:     print("{} 正在链接业务部门1数据接口。。。".format(name))
    ...:     

In [45]: f1("jack", 18)
认证成功!
jack 正在链接业务部门1数据接口。。。
日志添加成功!

    又绕晕了?其实你能够这么理解,先执行Filter函数,得到它的返回值outer,再执行@outer装饰器语法。


一个函数返回一个不带参数的装饰器就是带参数的装饰器


由于装饰器自身的参数不能直接传入因此在装饰器在装饰器外再封装一层函数的目地就是为了给装饰器传参,


4、更进一步的思考

一个函数能够被多个函数装饰吗?能够的!看下面的例子!

In [25]: def outer1(func):  
    ...:     def inner(*args, **kwargs):
    ...:         print('认证成功')          
    ...:         result = func(*args, **kwargs)
    ...:         print('日志添加成功')      
    ...:         return result
    ...:     return inner
    ...: 
In [25]: def outer1(func):
    ...:     def inner(*args, **kwargs):
    ...:         print('认证成功')
    ...:         result = func(*args, **kwargs)
    ...:         print('日志添加成功')
    ...:         return result
    ...:     return inner
    ...: 

In [26]: def outer2(func):
    ...:     def inner(*args, **kwargs):
    ...:         print('一条欢迎信息。。。')
    ...:         result = func(*args, **kwargs)
    ...:         print('一条欢送信息。。。')
    ...:         return result
    ...:     return inner
    ...: 

In [27]: @outer1
    ...: @outer2
    ...: def f1(name, age):
    ...:     print("{} 正在链接业务部门1数据接口。。.".format(name))
    ...:     

In [28]: f1("jack", 18)
认证成功
一条欢迎信息。。。
jack 正在链接业务部门1数据接口。。.
一条欢送信息。。。
日志添加成功
相关文章
相关标签/搜索