詳解Python裝飾器的介紹

高洛峰
發布: 2017-03-23 16:35:16
原創
1517 人瀏覽過

裝飾器的功能在許多語言中都有,名字也不盡相同,其實它體現的是一種設計模式,強調的是開放封閉原則,更多的用於後期功能升級而不是編寫新的代碼。裝飾器不光能裝飾函數,也能裝飾其他的對象,例如類,但通常,我們以裝飾函數為例子介紹其用法。要理解在Python中裝飾器的原理,需要一步一步來。本文盡量描述得淺顯易懂,從最基礎的內容講起。
(註:以下使用Python3.5.1環境)
一、Python的函數相關基礎
第一,必須強調的是python是從上往下順序執行的,而且碰到函數的定義程式碼區塊是不會立即執行它的,只有等到函數被呼叫時,才會執行其內部的程式碼區塊。

def foo():
print("foo函数被运行了!") 
如果就这么样,foo里的语句是不会被执行的。
程序只是简单的将定义代码块读入内存中。
登入後複製


再看看,順序執行的例子:

def foo():
 print("我是上面的函数定义!")
def foo():
 print("我是下面的函数定义!")
foo()
运行结果:
我是下面的函数定义
登入後複製


#可見,因為順序執行的原因,下面的foo將上面的foo覆蓋了。因此,在Python中程式碼的放置位置是有要求的,不能隨意擺放,函數體要放在被呼叫的語句之前。
 其次,我們還要先搞清楚幾樣東西:函數名、函數體、回傳值,函數的記憶體位址、函數名稱加括號、函數名被當作參數、函數名加括號被當作參數、傳回函數名、傳回函數名加括號。對於以下的函數:

def foo():
 print("让我们干点啥!")
 return "ok"
 foo()  
登入後複製


    函數名稱:        foo
  函數體:       字串「ok」    如果不明確給出return的對象,那麼預設回傳None
  函數的記憶體位址:    當函數體被讀進記憶體後的保存位置,它由標識符即函數名稱foo引用,
                  也就是說foo指向的是函數體在記憶體內的保存位置。
  函數名稱加括號:       例如foo(),函數的調用方法,只有見到這個括號,程式會根據
                  函數名稱從記憶體中找到函數體,然後執行它
再看下面這個例子:

def outer(func):
 def inner():
 print("我是内层函数!")
 return inner
def foo():
 print("我是原始函数!") 
outer(foo)
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,有沒有括號是兩個截然不同的意思!

二、裝飾器的使用場景    裝飾器通常用於在不改變原有函數程式碼和功能的情況下,為其添加額外的功能。例如在原函數執行前先執行點什麼,執行後執行點什麼。
 讓我們透過一個例子來看看,裝飾器的使用場景和體現的設計模式。 (抱歉的是我設計不出更好的場景,只能引用武大神的案例加以演繹)
 有一個大公司,下屬的基礎平台部負責內部應用程序及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 、f2、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()
登入後複製


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

#/usr/bin/env python
#coding:utf-8
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 之前都进行认证操作,在操作结束后保存日志,并且其他业务部门无需他们自己的代码做任何修改,调用方式也不用变。“主管”写完代码后,觉得独乐了不如众乐乐,打算显摆一下,于是写了篇博客将过程进行了详细的说明。
三、装饰器的内部原理、
 下面我们以f1函数为例进行说明:

def outer(func):
 def inner():
 print("认证成功!")
 result = func()
 print("日志添加成功")
 return result
 return inner
@outer
def f1():
 print("业务部门1数据接口......")
登入後複製


运用我们在第一部分介绍的知识来分析一下上面这段代码:
 程序开始运行,从上往下编译,读到def outer(func):的时候,发现这是个“一等公民”->函数,于是把函数体加载到内存里,然后过。
 读到@outer的时候,程序被@这个语法糖吸引住了,知道这是个装饰器,按规矩要立即执行的,于是程序开始运行@后面那个名字outer所定义的函数。(相信没有人会愚蠢的将@outer写到别的位置,它只能放在被装饰的函数的上方最近处,不要空行。)
 程序返回到outer函数,开始执行装饰器的语法规则,这部分规则是定死的,是python的“法律”,不要问为什么。规则是:被装饰的函数的名字会被当作参数传递给装饰函数。装饰函数执行它自己内部的代码后,会将它的返回值赋值给被装饰的函数。 
如下图所示:
詳解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函数内部的代码,如果不封装一下,在业务部门还未进行调用的时候,就执行了些什么,这和初衷有点不符。当然,如果你对这个有需求也不是不行。请看下面的例子,它只有一层函数。

def outer(func):
 print("认证成功!")
 result = func()
 print("日志添加成功")
 return result
@outer
def f1():
 print("业务部门1数据接口......")
# 业务部门并没有开始执行f1函数
执行结果:
认证成功!
业务部门1数据接口......
日志添加成功
登入後複製


看到没?我只是定义好了函数,业务部门还没有调用f1函数呢,程序就把工作全做了。这就是封装一层函数的原因。
四、装饰器的参数传递
细心的朋友可能已经发现了,上面的例子中,f1函数没有参数,在实际情况中肯定会需要参数的,那参数怎么传递的呢?
 一个参数的情况:

def outer(func):
 def inner(username):
 print("认证成功!")
 result = func(username)
 print("日志添加成功")
 return result
 return inner
@outer
def f1(name):
print("%s 正在连接业务部门1数据接口......"%name)
# 调用方法
f1("jack")
登入後複製

在inner函数的定义部分也加上一个参数,调用func函数的时候传递这个参数,很好理解吧?可问题又来了,那么另外一个部门调用的f2有2个参数呢?f3有3个参数呢?你怎么传递?
很简单,我们有*args和**kwargs嘛!号称“万能参数”!简单修改一下上面的代码:

def outer(func):
 def inner(*args,**kwargs):
 print("认证成功!")
 result = func(*args,**kwargs)
 print("日志添加成功")
 return result
 return inner
@outer
def f1(name,age):
 print("%s 正在连接业务部门1数据接口......"%name)
# 调用方法
f1("jack",18)
登入後複製

五、更进一步的思考
 一个函数可以被多个函数装饰吗?可以的!看下面的例子!  

def outer1(func):
 def inner(*args,**kwargs):
 print("认证成功!")
 result = func(*args,**kwargs)
 print("日志添加成功")
 return result
 return inner
def outer2(func):
 def inner(*args,**kwargs):
 print("一条欢迎信息。。。")
 result = func(*args,**kwargs)
 print("一条欢送信息。。。")
 return result
 return inner
 @outer1
@outer2
def f1(name,age):
 print("%s 正在连接业务部门1数据接口......"%name)
# 调用方法
f1("jack",18) 
执行结果:
认证成功!
一条欢迎信息。。。
jack 正在连接业务部门1数据接口......
一条欢送信息。。。
日志添加成功
登入後複製

更进一步的,装饰器自己可以有参数吗?可以的!看下面的例子:

# 认证函数
def auth(request,kargs):
 print("认证成功!")
# 日志函数
def log(request,kargs):
 print("日志添加成功")
# 装饰器函数。接收两个参数,这两个参数应该是某个函数的名字。
def Filter(auth_func,log_func):
 # 第一层封装,f1函数实际上被传递给了main_fuc这个参数
 def outer(main_func):
 # 第二层封装,auth和log函数的参数值被传递到了这里
 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
@Filter(auth,log)
def f1(name,age):
 print("%s 正在连接业务部门1数据接口......"%name)
# 调用方法
f1("jack",18)
运行结果:
认证成功!
jack 正在连接业务部门1数据接口......
日志添加成功
登入後複製

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

以上是詳解Python裝飾器的介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!