理解 Python 中的裝飾器

jopen 11年前發布 | 39K 次閱讀 Python Python開發

1、問題

文章先由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> </pre>現在我們來看看如何從一些最基礎的方式來理解Python的裝飾器。英文討論參考  Here

1.1. 需求是怎么來的? 

裝飾器是一個很著名的設計模式,經常被用于有切面需求的場景,較為經典的有插入日志、性能測試、事務處理等。裝飾器是解決這類問題的絕佳設計,有了裝飾器,我們就可以抽離出大量函數中與函數功能本身無關的雷同代碼并繼續重用。概括的講,裝飾器的作用就是為已經存在的對象添加額外的功能。

裝飾器的定義很是抽象,我們來看一個小例子。 

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

這是一個很無聊的函數沒錯。但是突然有一個更無聊的人,我們稱呼他為B君,說我想看看執行這個函數用了多長時間,好吧,那么我們可以這樣做: 

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

foo() </pre>很好,功能看起來無懈可擊。可是蛋疼的B君此刻突然不想看這個函數了,他對另一個叫foo2的函數產生了更濃厚的興趣。 

怎么辦呢?如果把以上新增加的代碼復制到foo2里,這就犯了大忌了~復制什么的難道不是最討厭了么!而且,如果B君繼續看了其他的函數呢? 

1.2. 以不變應萬變,是變也  

還記得嗎,函數在Python中是一等公民,那么我們可以考慮重新定義一個函數timeit,將foo的引用傳遞給他,然后在timeit中調用foo并進行計時,這樣,我們就達到了不改動foo定義的目的,而且,不論B君看了多少個函數,我們都不用去修改函數定義了! 

import time

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

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

timeit(foo) </pre>看起來邏輯上并沒有問題,一切都很美好并且運作正常!……等等,我們似乎修改了調用部分的代碼。原本我們是這樣調用的:foo(),修改以后變成了:timeit(foo)。這樣的話,如果foo在N處都被調用了,你就不得不去修改這N處的代碼。或者更極端的,考慮其中某處調用的代碼無法修改這個情況,比如:這個函數是你交給別人使用的。 

1.3. 最大限度地少改動!  

既然如此,我們就來想想辦法不修改調用的代碼;如果不修改調用代碼,也就意味著調用foo()需要產生調用timeit(foo)的效果。我們可以想到將timeit賦值給foo,但是timeit似乎帶有一個參數……想辦法把參數統一吧!如果timeit(foo)不是直接產生調用效果,而是返回一個與foo參數列表一致的函數的話……就很好辦了,將timeit(foo)的返回值賦值給foo,然后,調用foo()的代碼完全不用修改! 

#-- coding: UTF-8 --
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() </pre>這樣,一個簡易的計時器就做好了!我們只需要在定義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() </pre>

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

1.4 最后回答前面提到的問題: 

# 裝飾器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></pre>

2、裝飾器的種類

2.1 無參數裝飾器

def deco(func):  
    print func  
    return func  
@deco  
def foo():pass  
foo()  

第一個函數deco是裝飾函數,它的參數就是被裝飾的函數對象。我們可以在deco函數內對傳入的函數對象做一番“裝飾”,然后返回這個對象(記住一定要返回 ,不然外面調用foo的地方將會無函數可用。實際上此時foo=deco(foo))

我寫了個小例子,檢查函數有沒有說明文檔:

def deco_functionNeedDoc(func):  
    if func.__doc__ == None :  
        print func, "has no __doc__, it's a bad habit."  
    else:  
        print func, ':', func.__doc__, '.'  
    return func  
@deco_functionNeedDoc  
def f():  
    print 'f() Do something'  
@deco_functionNeedDoc  
def g():  
    'I have a __doc__'  
    print 'g() Do something'  
f()  
g()  

2.2 有參數裝飾器

def decomaker(arg):  
    '通常對arg會有一定的要求'  
    """由于有參數的decorator函數在調用時只會使用應用時的參數  
       而不接收被裝飾的函數做為參數,所以必須在其內部再創建  
       一個函數  
    """  
    def newDeco(func):    #定義一個新的decorator函數  
        print func, arg  
        return func  
    return newDeco  
@decomaker(deco_args)  
def foo():pass  
foo() 

第一個函數decomaker是裝飾函數,它的參數是用來加強“加強裝飾”的。由于此函數并非被裝飾的函數對象,所以在內部必須至少創建一個接受被裝飾函數的函數,然后返回這個對象(實際上此時foo=decomaker(arg)(foo))

這個我還真想不出什么好例子,還是見識少啊,只好借用同步鎖的例子了:

def synchronized(lock):  
    """鎖同步裝飾方法 
    !lock必須實現了acquire和release方法 
    """  
    def sync_with_lock(func):  
        def new_func(*args, **kwargs):  
            lock.acquire()  
            try:  
                return func(*args, **kwargs)  
            finally:  
                lock.release()  
        new_func.func_name = func.func_name  
        new_func.__doc__ = func.__doc__  
        return new_func  
    return sync_with_lock  
@synchronized(__locker)  
def update(data):  
"""更新計劃任務"""  
    tasks = self.get_tasks()  
    delete_task = None  
    for task in tasks:  
        if task[PLANTASK.ID] == data[PLANTASK.ID]:  
            tasks.insert(tasks.index(task), data)  
            tasks.remove(task)  
            delete_task = task  
    r, msg = self._refresh(tasks, delete_task)  
    return r, msg, data[PLANTASK.ID]  

調用時還是updae(data)。

 同時還可以將多個裝飾器組合 使用,注意調用順序:

@synchronized(__locker)  
@deco_functionNeedDoc  
def f():  
    print 'f() Do something'  

2.3 內置的裝飾器

</div>

內置的裝飾器有三個,分別是staticmethod、classmethod和property,作用分別是把類中定義的實例方法變成靜態方法、類方法和類屬性。由于模塊里可以定義函數,所以靜態方法和類方法的用處并不是太多,除非你想要完全的面向對象編程。而屬性也不是不可或缺的,Java沒有屬性也一樣活得很滋潤。從我個人的Python經驗來看,我沒有使用過property,使用staticmethod和classmethod的頻率也非常低。

具體請參考: http://www.cnblogs.com/huxi/archive/2011/03/01/1967600.html


3、REF:

1、可愛的 Python: Decorator 簡化元編程

http://www.ibm.com/developerworks/cn/linux/l-cpdecor.html

2、Python的裝飾器

http://jnotes.googlecode.com/svn/trunk/Notes/NotesOnPythonLearning/Python_decorator.html

3、Python裝飾器學習

http://blog.csdn.net/thy38/article/details/4471421

4、通過 Python 裝飾器實現DRY(不重復代碼)原則

http://www.oschina.net/translate/dry-principles-through-python-decorators

5、http://simeonfranklin.com/blog/2012/jul/1/python-decorators-in-12-steps/

</div> 原文地址:http://hmw.iteye.com/blog/1510673

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!