Python中的裝飾器及@用法詳解

NO IMAGE

轉載請註明出處:http://blog.csdn.net/tyhj_sf/article/details/77417455

這篇文章主要介紹了Python中的裝飾器用法,以例項形式詳細的分析了Python中的裝飾器的使用技巧及相關注意事項。

一、例子

1.1 先看下面一個簡單的例子程式

def funA(arg):
print 'A'
a=arg()
@funA
def funB():
print 'B'

執行結果:

A
B

此處的@相當於funA(funB())。

1.2 再看一個複雜點的例子:
來自stackoverflow上面的一個問題,如果使用如下的程式碼:

@makebold
@makeitalic
def say():
return "Hello"

列印出如下的輸出:

<b><i>Hello<i></b>

你會怎麼做?最後給出的答案是:

def makebold(fn):
def wrapped():
return "<b>"   fn()   "</b>"
return wrapped
def makeitalic(fn):
def wrapped():
return "<i>"   fn()   "</i>"
return wrapped
@makebold
@makeitalic
def hello():
return "hello world"
print hello() ## 返回 <b><i>hello world</i></b>

二、概念

2.1 裝飾器

裝飾器是一個很著名的設計模式,經常被用於有切面需求的場景,較為經典的應用有插入日誌、增加計時邏輯來檢測效能、加入事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函式中與函式功能本身無關的雷同程式碼並繼續重用。概括的講,裝飾器的作用就是為已經存在的物件新增額外的功能。

2.2 裝飾器應用的由來

裝飾器的定義很是抽象,我們來看一個小例子。
先定義一個簡單的函式:

def foo():
print 'in foo()'
foo()

然後呢,我想看看執行這個函式用了多長時間,好吧,那麼我們可以這樣做:

import time
def foo():
start = time.clock()
print 'in foo()'
end = time.clock()
print 'used:', end - start
foo()

很好,功能看起來無懈可擊。但是如果我要看看另外一個函式的執行效能,計算時間的這些與函式本身功能無關的程式碼我還得寫一遍,這很不軟體工程啊,怎麼辦呢?
還記得嗎,函式在Python中是一等公民,那麼我們可以考慮重新定義一個函式timeit,將foo的引用傳遞給他,然後在timeit中呼叫foo並進行計時,這樣,我們就達到了不改動foo定義的目的

import time
def foo():
print 'in foo()'
def timeit(func):
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
timeit(foo)

看起來邏輯上並沒有問題,一切都很美好並且運作正常!但是,我們似乎修改了呼叫部分的程式碼。原本我們是這樣呼叫的:foo(),修改以後變成了:timeit(foo)。這樣的話,如果foo在N處都被呼叫了,你就不得不去修改這N處的程式碼。或者更極端的,考慮其中某處呼叫的程式碼無法修改這個情況,比如:這個函式是你交給別人使用的。
既然如此,我們就來想想辦法不修改呼叫的程式碼;如果不修改呼叫程式碼,也就意味著呼叫foo()需要產生呼叫timeit(foo)的效果。我們可以想到將timeit賦值給foo,但是timeit似乎帶有一個引數……想辦法把引數統一吧!如果timeit(foo)不是直接產生呼叫效果,而是返回一個與foo引數列表一致的函式的話……就很好辦了,將timeit(foo)的返回值賦值給foo,然後,呼叫foo()的程式碼完全不用修改!

import time
def foo():
print 'in foo()'

定義一個計時器,傳入一個,並返回另一個附加了計時功能的方法

def timeit(func):
# 定義一個內嵌的包裝函式,給傳入的函式加上計時功能的包裝
def wrapper():
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
# 將包裝後的函式返回
return wrapper
foo = timeit(foo)#傳入的引數foo為原函式名,變數foo為新變數不是原函式
foo()

這樣,一個簡易的計時器就做好了!我們只需要在定義foo以後呼叫foo之前,加上foo = timeit(foo),就可以達到計時的目的,這也就是裝飾器的概念,看起來像是foo被timeit裝飾了。

在這個例子中,函式進入和退出時需要計時,這被稱為一個橫切面(Aspect),這種程式設計方式被稱為面向切面的程式設計(Aspect-Oriented Programming)。與傳統程式設計習慣的從上往下執行方式相比較而言,像是在函式執行的流程中橫向地插入了一段邏輯。在特定的業務領域裡,能減少大量重複程式碼。面向切面程式設計還有相當多的術語,這裡就不多做介紹,感興趣的話可以去找找相關的資料。

這個例子僅用於演示,並沒有考慮foo帶有引數和有返回值的情況,完善它的重任就交給讀者你最為練習 ,可以檢驗下你是否真正理解了學會了。

上面這段程式碼看起來似乎已經不能再精簡了,Python於是提供了一個語法糖來降低字元輸入量。

import time
def timeit(func):
def wrapper():
start = time.clock()
func()
end =time.clock()
print 'used:', end - start
return wrapper
@timeit
def foo():
print 'in foo()'
foo()

重點關注第11行的@timeit,在定義上加上這一行與另外寫foo = timeit(foo)完全等價,千萬不要以為@有另外的魔力。除了字元輸入少了一些,還有一個額外的好處:這樣看上去更有裝飾器的感覺。

三、函式引用

要理解python的裝飾器,我們首先必須明白在Python中函式也是被視為物件。這一點很重要。先看一個例子:

def shout(word="yes") :
return word.capitalize() " !"
print shout()
# 輸出 : 'Yes !'
# 作為一個物件,你可以把函式賦給任何其他物件變數 
scream = shout
# 注意我們沒有使用圓括號,因為我們不是在呼叫函式
# 我們把函式shout賦給scream,也就是說你可以通過scream呼叫shout
print scream()
# 輸出 : 'Yes !'
# 還有,你可以刪除舊的名字shout,但是你仍然可以通過scream來訪問該函式
del shout
try :
print shout()
except NameError, e :
print e
#輸出 : "name 'shout' is not defined"
print scream()
# 輸出 : 'Yes !'

我們暫且把這個話題放旁邊,我們先看看python另外一個很有意思的屬性:可以在函式中定義函式:

def talk() :
# 你可以在talk中定義另外一個函式
def whisper(word="yes") :
return word.lower() "...";
# ... 並且立馬使用它
print whisper()
# 你每次呼叫'talk',定義在talk裡面的whisper同樣也會被呼叫
talk()
# 輸出 :
# yes...
# 但是"whisper" 不會單獨存在:
try :
print whisper()
except NameError, e :
print e
#輸出 : "name 'whisper' is not defined"*

從以上兩個例子我們可以得出,函式既然作為一個物件,因此:

    其可以被賦給其他變數
其可以被定義在另外一個函式內

這也就是說,函式可以返回一個函式,看下面的例子:

def getTalk(type="shout") :
# 我們定義另外一個函式
def shout(word="yes") :
return word.capitalize() " !"
def whisper(word="yes") :
return word.lower() "...";
# 然後我們返回其中一個
if type == "shout" :
# 我們沒有使用(),因為我們不是在呼叫該函式
# 我們是在返回該函式
return shout
else :
return whisper
# 然後怎麼使用呢 ?
# 把該函式賦予某個變數
talk = getTalk()     
# 這裡你可以看到talk其實是一個函式物件:
print talk
#輸出 : <function shout at 0xb7ea817c>
# 該物件由函式返回的其中一個物件:
print talk()
# 或者你可以直接如下呼叫 :
print getTalk("whisper")()
#輸出 : yes...
<p></p>

還有,既然可以返回一個函式,我們可以把它作為引數傳遞給函式:

def doSomethingBefore(func) :
print "I do something before then I call the function you gave me"
print func()
doSomethingBefore(scream)
#輸出 :
#I do something before then I call the function you gave me
#Yes !

這裡你已經足夠能理解裝飾器了,其他它可被視為封裝器。也就是說,它能夠讓你在裝飾前後執行程式碼而無須改變函式本身內容。

四、手工裝飾

那麼如何進行手動裝飾呢?

# 裝飾器是一個函式,而其引數為另外一個函式
def my_shiny_new_decorator(a_function_to_decorate) :
# 在內部定義了另外一個函式:一個封裝器。
# 這個函式將原始函式進行封裝,所以你可以在它之前或者之後執行一些程式碼
def the_wrapper_around_the_original_function() :
# 放一些你希望在真正函式執行前的一些程式碼
print "Before the function runs"
# 執行原始函式
a_function_to_decorate()
# 放一些你希望在原始函式執行後的一些程式碼
print "After the function runs"
#在此刻,"a_function_to_decrorate"還沒有被執行,我們返回了建立的封裝函式
#封裝器包含了函式以及其前後執行的程式碼,其已經準備完畢
return the_wrapper_around_the_original_function
# 現在想象下,你建立了一個你永遠也不遠再次接觸的函式
def a_stand_alone_function() :
print "I am a stand alone function, don't you dare modify me"
a_stand_alone_function()
#輸出: I am a stand alone function, don't you dare modify me
# 好了,你可以封裝它實現行為的擴充套件。可以簡單的把它丟給裝飾器
# 裝飾器將動態地把它和你要的程式碼封裝起來,並且返回一個新的可用的函式。
a_stand_alone_function_decorated = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function_decorated()
#輸出 :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs

現在你也許要求當每次呼叫a_stand_alone_function時,實際呼叫卻是a_stand_alone_function_decorated。實現也很簡單,可以用my_shiny_new_decorator來給a_stand_alone_function重新賦值。

a_stand_alone_function = my_shiny_new_decorator(a_stand_alone_function)
a_stand_alone_function()
#輸出 :
#Before the function runs
#I am a stand alone function, don't you dare modify me
#After the function runs
# And guess what, that's EXACTLY what decorators do !

五、裝飾器揭祕

前面的例子,我們可以使用裝飾器的語法:

@my_shiny_new_decorator
def another_stand_alone_function() :
print "Leave me alone"
another_stand_alone_function()
#輸出 :
#Before the function runs
#Leave me alone
#After the function runs

當然你也可以累積裝飾:

def bread(func) :
def wrapper() :
print "</''''''\>"
func()
print "<\______/>"
return wrapper
def ingredients(func) :
def wrapper() :
print "#tomatoes#"
func()
print "~salad~"
return wrapper
def sandwich(food="--ham--") :
print food
sandwich()
#輸出 : --ham--
sandwich = bread(ingredients(sandwich))
sandwich()
#outputs :
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

使用python裝飾器語法:

@bread
@ingredients
def sandwich(food="--ham--") :
print food
sandwich()
#輸出 :
#</''''''\>
# #tomatoes#
# --ham--
# ~salad~
#<\______/>

裝飾器的順序很重要,需要注意:

@ingredients
@bread
def strange_sandwich(food="--ham--") :
print food
strange_sandwich()
#輸出 :
##tomatoes#
#</''''''\>
# --ham--
#<\______/>
# ~salad~

最後回答前面提到的問題:

# 裝飾器makebold用於轉換為粗體
def makebold(fn):
# 結果返回該函式
def wrapper():
# 插入一些執行前後的程式碼
return "<b>"   fn()   "</b>"
return wrapper
# 裝飾器makeitalic用於轉換為斜體
def makeitalic(fn):
# 結果返回該函式
def wrapper():
# 插入一些執行前後的程式碼
return "<i>"   fn()   "</i>"
return wrapper
@makebold
@makeitalic
def say():
return "hello"
print say()
#輸出: <b><i>hello</i></b>
# 等同於
def say():
return "hello"
say = makebold(makeitalic(say))
print say()
#輸出: <b><i>hello</i></b>

六、內建的裝飾器

內建的裝飾器有三個,分別是staticmethod、classmethod和property,作用分別是把類中定義的例項方法變成靜態方法、類方法和類屬性。由於模組裡可以定義函式,所以靜態方法和類方法的用處並不是太多,除非你想要完全的物件導向程式設計。而屬性也不是不可或缺的,Java沒有屬性也一樣活得很滋潤。從我個人的Python經驗來看,我沒有使用過property,使用staticmethod和classmethod的頻率也非常低。

class Rabbit(object):
def __init__(self, name):
self._name = name
@staticmethod
def newRabbit(name):
return Rabbit(name)
@classmethod
def newRabbit2(cls):
return Rabbit('')
@property
def name(self):
return self._name

這裡定義的屬性是一個只讀屬性,如果需要可寫,則需要再定義一個setter:

@name.setter
def name(self, name):
self._name = name

6.1 functools模組

functools模組提供了兩個裝飾器。這個模組是Python 2.5後新增的,一般來說大家用的應該都高於這個版本。

6.1.1 wraps(wrapped[, assigned][, updated]):

這是一個很有用的裝飾器。看過前一篇反射的朋友應該知道,函式是有幾個特殊屬性比如函式名,在被裝飾後,上例中的函式名foo會變成包裝函式的名字wrapper,如果你希望使用反射,可能會導致意外的結果。這個裝飾器可以解決這個問題,它能將裝飾過的函式的特殊屬性保留。

import time import functools 
def timeit(func): 
@functools.wraps(func) 
def wrapper(): 
start = time.clock() 
func()
end =time.clock()
print ‘used:’, end - start
return wrapper
@timeit
def foo(): 
print ‘in foo()’
foo()
print foo.__name__

首先注意第5行,如果註釋這一行,foo.name將是’wrapper’。另外相信你也注意到了,這個裝飾器竟然帶有一個引數。實際上,他還有另外兩個可選的引數,assigned中的屬性名將使用賦值的方式替換,而updated中的屬性名將使用update的方式合併,你可以通過檢視functools的原始碼獲得它們的預設值。對於這個裝飾器,相當於wrapper = functools.wraps(func)(wrapper)。

6.1.2 total_ordering(cls):

這個裝飾器在特定的場合有一定用處,但是它是在Python 2.7後新增的。它的作用是為實現了至少ltlegtge其中一個的類加上其他的比較方法,這是一個類裝飾器。如果覺得不好理解,不妨仔細看看這個裝飾器的原始碼:

def total_ordering(cls):
"""Class decorator that fills in missing ordering methods"""
convert = {
'__lt__': [('__gt__', lambda self, other: other < self),
('__le__', lambda self, other: not other < self),
('__ge__', lambda self, other: not self < other)],
'__le__': [('__ge__', lambda self, other: other <= self),
('__lt__', lambda self, other: not other <= self),
('__gt__', lambda self, other: not self <= other)],
'__gt__': [('__lt__', lambda self, other: other > self),
('__ge__', lambda self, other: not other > self),
('__le__', lambda self, other: not self > other)],
'__ge__': [('__le__', lambda self, other: other >= self),
('__gt__', lambda self, other: not other >= self),
('__lt__', lambda self, other: not self >= other)]
}
roots = set(dir(cls)) & set(convert)
if not roots:
raise ValueError('must define at least one ordering operation: < > <= >=')
root = max(roots)       # prefer __lt__ to __le__ to __gt__ to __ge__
for opname, opfunc in convert[root]:
if opname not in roots:
opfunc.__name__ = opname
opfunc.__doc__ = getattr(int, opname).__doc__
setattr(cls, opname, opfunc)
return cls