Python裝飾器以及高級用法

介紹

首先我要認可,裝飾器很是難!你在本教程中看到的一些代碼將會有一些複雜。大多數人在學習Python時都跟裝飾器作過鬥爭,因此若是這對你來講很奇怪,不要感到沮喪,由於一樣的大多數人均可以克服這種苦難。在本教程中,我將逐步介紹瞭解裝飾器的過程。首先我假設你已經能夠編寫基本函數和基本類。若是你不能作這些事,那麼我建議你在回到這裏以前先學習如何去作到編寫基本函數和基本類(除非你迷路了,在這種狀況下你能夠原諒)。python

用例:計時函數執行

假設咱們正在執行一段代碼,執行時間比咱們想的還要長一些。這段代碼由一堆函數調用組成,咱們確信這些調用中至少有一個調用構成了咱們代碼中的瓶頸。咱們如何找到瓶頸?如今有一個解決方案,就是咱們如今要關注的解決方案,就是對函數執行進行計時。數據庫

讓咱們從一個簡單的例子開始。咱們只有一個函數須要計時,func_a設計模式

def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()

一種方法是將時鐘代碼放在每一個函數調用周圍。因此就像這樣:框架

func_a(current_stuff)

看起來會更像這樣:ide

before = datetime.datetime.now()
func_a(current_stuff)
after = datetime.datetime.now()
print ("Elapsed Time = {0}".format(after-before))

這樣就能夠了。可是若是咱們有屢次調用func_a而且咱們想要爲全部這些計時會發生什麼呢?咱們能夠用計時代碼包圍func_a的每一個調用,可是這樣作也有很差的效果。它只准備編寫一次計時代碼。所以,咱們將其放在函數定義中,而不是將其放在函數以外。函數

def func_a(stuff):
    before = datetime.datetime.now()
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

這種方法的好處是:學習

  1. 咱們將代碼放在一個地方,因此若是咱們想要更改它(例如,若是咱們想將通過的時間存儲在數據庫或日誌中)那麼咱們只須要在一個地方而不是每個函數調用中更改它
  2. 咱們不須要記住每次調用func_a都要寫四行代碼而不是一行,這是很是好的

好的,可是隻須要計算一個函數的時間是不現實的。若是你須要對一件事進行計時,你頗有可能須要至少對兩件事進行計時。因此咱們會選擇三個。測試

def func_a(stuff):
    before = datetime.datetime.now()
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

def func_b(stuff):
    before = datetime.datetime.now()
    do_important_things_4()
    do_important_things_5()
    do_important_things_6()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

def func_c(stuff):
    before = datetime.datetime.now()
    do_important_things_7()
    do_important_things_8()
    do_important_things_9()
    after = datetime.datetime.now()
    print("Elapsed Time = {0}".format(after-before))

這看起來很糟糕。若是咱們想要對8個函數進行計時的時候怎麼辦?而後咱們決定將計時的信息存儲在日誌文件中。而後咱們決定創建一個更好的數據庫。咱們這裏須要的是將一種相同的代碼合併到func_afunc_bfunc_c中的方法,這種方法不會讓咱們處處複製粘貼代碼。ui

一個簡單的繞道:返回函數的函數

Python是一種很是特殊的語言,由於函數是第一類對象。這意味着一旦函數在做用域中被定義,它就能夠傳遞給函數,賦值給變量,甚至從函數返回。這個簡單的事實是使python裝飾器成爲可能的緣由。查看下面的代碼,看看你是否能夠猜出標記爲A,B,C和D的行會發生什麼。this

def get_function():
    print ("inside get_function")                 
    def returned_function():                    
        print("inside returned_function")        
        return 1
    print("outside returned_function")
    return returned_function

returned_function()     # A                         
x = get_function()      # B                         
x                       # C                        
x()                     # D

A

這一行給出了一個NameError並聲明returned_function不存在。但咱們只是定義了它,對吧?你在這裏須要知道的是,它是在get_function的範圍內定義的。也就是說,在get_function裏面定義了它。它不是在get_function以外。若是這讓你感到困惑,那麼你能夠嘗試使用該locals()函數,並閱讀Python的範圍。

B

這行代碼打印出如下內容:

inside get_function
outside returned_function

此時Python不執行returned_function的任何內容。

C

這一行輸出:

<function returned_function at 0x7fdc4463f5f0>

也就是說,get_function()返回的值x自己就是一個函數。

嘗試再次運行B和C行。請注意,每次重複此過程時,返回的returned_function地址都是不一樣。每次調用get_function都會生成新的returned function

d

由於x是函數,因此就能夠調用它。調用x就是調用returned_function的一個實例。這裏輸出的是:

inside returned_function
1

也就是說,它打印字符串,並返回值1

回到時間問題

你如今仍然在看麼?如此咱們有了新的知識,那麼咱們如何解決咱們的老問題?我建議咱們建立一個函數,讓咱們調用它並稱爲time_this,它將接收另外一個函數做爲參數,並將參數函數封裝在某些計時代碼中。有點像:

def time_this(original_function):                            # 1
    def new_function(*args,**kwargs):                        # 2
        before = datetime.datetime.now()                     # 3
        x = original_function(*args,**kwargs)                # 4
        after = datetime.datetime.now()                      # 5
        print("Elapsed Time = {0}".format(after-before))     # 6
        return x                                             # 7
    return new_function()                                    # 8

我認可它有點瘋狂,因此讓咱們一行一行的看下去:

1這只是time_this的原型。time_this是一個函數就像任何其餘函數同樣,而且只有一個參數。 2咱們在內部定義一個函數time_this。每當time_this執行時它都會建立一個新函數。 3計時代碼,就像以前同樣。 4咱們調用原始函數並保留結果以供往後使用。 5,6剩餘的計時代碼。 7new_function必須像原始函數同樣運行,所以返回存儲的結果。 8返回在time_this中建立的函數。

如今咱們要確保咱們的函數是計時的:

def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
func_a = time_this(func_a)        # <---------

def func_b(stuff):
    do_important_things_4()
    do_important_things_5()
    do_important_things_6()
func_b = time_this(func_b)        # <---------

def func_c(stuff):
    do_important_things_7()
    do_important_things_8()
    do_important_things_9()
func_c = time_this(func_c)        # <---------

看看func_a,當咱們執行時func_a = time_this(func_a)咱們用time_this返回的函數替換func_a。因此咱們用一個函數替換func_A該函數執行一些計時操做(上面的第3行),將func a的結果存儲在一個名爲x的變量中(第4行),執行更多的計時操做(第5行和第6行),而後返回func_a返回的內容。換句話說func_a,仍然以相同的方式調用並返回相同的東西,它也只是被計時了。是否是感受很整潔?

介紹裝飾器

咱們所作的工做很好,並且很是棒,可是很難看,很是難讀懂。因此Python可愛的做者給了咱們一種不一樣的,更漂亮的寫做方式:

@time_this
def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()

徹底等同於:

def func_a(stuff):
    do_important_things_1()
    do_important_things_2()
    do_important_things_3()
func_a = time_this(func_a)

這一般被稱爲語法糖。@沒有什麼神奇的。這只是一個已達成一致的慣例。沿着這條路上的某個地方決定了。

總結

裝飾器只是一個返回函數的函數。若是這些東西看起來很是的 - 那麼請確保如下主題對你有意義而後再回到本教程:

  • Python函數
  • 範圍
  • Python做爲第一類對象(甚至能夠查找lambda函數,它可能使它更容易理解)。

另外一方面,若是你對更多的話題感興趣的話,你可能會發現:

  • 如裝飾類:

python @add_class_functionality class MyClass: ...

  • 具備更多參數的裝飾器, 例如:

python @requires_permission(name="edit") def save_changes(stuff): ...

下面就是我要介紹的高級裝飾器的主題。

裝飾器的高級用法

介紹

下面這些旨在介紹裝飾器的一些更有趣的用法。具體來講,如何在類上使用裝飾器,以及如何將額外的參數傳遞給裝飾器函數。

裝飾者與裝飾者模式

裝飾器模式是一種面向對象的設計模式,其容許動態地將行爲添加到現有的對象當中。當你裝飾對象時,你將以獨立於同類的其餘實例方式擴展它的功能。

Python裝飾器不是裝飾器模式的實現。Python裝飾器在定義時向函數和方法添加功能,它們不用於在運行時添加功能。裝飾器模式自己能夠在Python中實現,但因爲Python是Duck-teped的,所以這是一件很是簡單的事情。

一個基本的裝飾

這是裝飾器能夠作的一個很是基本的例子。我只是把它做爲一個參考點。在繼續以前,請確保你徹底理解這段代碼。

def time_this(original_function):      
    def new_function(*args,**kwargs):
        import datetime                 
        before = datetime.datetime.now()                     
        x = original_function(*args,**kwargs)                
        after = datetime.datetime.now()                      
        print ("Elapsed Time = {0}".format(after-before))    
        return x                                             
    return new_function                                   

@time_this
def func_a(stuff):
    import time
    time.sleep(3)

func_a(1)

接受參數的裝飾器

有時,除了裝飾的函數以外,裝飾器還可使用參數。這種技術常常用於函數註冊等事情。一個著名的例子是Pyramid Web應用程序框架中的視圖配置。例如:

@view_config(route_name='home', renderer='templates/mytemplate.pt')
def my_view(request):
    return {'project': 'hello decorators'}

假設咱們有一個應用程序,用戶能夠登陸並與一個漂亮的gui(圖形用戶界面)進行交互。用戶與gui的交互觸發事件,而這些事件致使Python函數被執行。讓咱們假設有不少用戶使用這個應用程序,而且他們有許多不一樣的權限級別。執行不一樣的功能須要不一樣的權限類型。例如,考慮如下功能:

#這些功能是存在的
def current_user_id():
    """
    此函數返回當前登陸的用戶ID,若是沒有通過身份驗證,則返回None 
    """

def get_permissions(iUserId):
    """
    返回給定用戶的權限字符串列表,例如 ['logged_in','administrator','premium_member']
    """

#咱們須要對這些函數進行權限檢查

def delete_user(iUserId):
   """
   刪除具備給定ID的用戶,只有管理員權限才能訪問此函數
   """

def new_game():
    """
    任何已登陸的用戶均可以啓動一個新遊戲
    """

def premium_checkpoint():
   """
   保存遊戲進程,只容許高級成員訪問
   """

實現這些權限的一種方法是建立多個裝飾器,例如:

def requires_admin(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'administrator' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn

def requires_logged_in(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'logged_in' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn

def requires_premium_member(fn):
    def ret_fn(*args,**kwargs):
        lPermissions = get_permissions(current_user_id())
        if 'premium_member' in lPermissions:
            return fn(*args,**kwargs)
        else:
            raise Exception("Not allowed")
    return ret_fn

@requires_admin
def delete_user(iUserId):
   """
   刪除具備給定Id的用戶,只有具備管理員權限的用戶才能訪問此函數
   """

@requires_logged_in 
def new_game():
    """
    任何已登陸的用戶均可以啓動一個新遊戲
    """

@requires_premium_member
def premium_checkpoint():
   """
   保存遊戲進程,只容許高級成員訪問
   """

但這太可怕了。它須要大量的複製粘貼,而且每一個裝飾器須要不一樣的名稱,若是對權限的檢查方式進行了任何更改,則必須更新每一個裝飾器。有一個裝飾器能夠完成這三個工做不是很好嗎?

爲此,咱們須要一個返回裝飾器的函數:

def requires_permission(sPermission):                            
    def decorator(fn):                                            
        def decorated(*args,**kwargs):                            
            lPermissions = get_permissions(current_user_id())     
            if sPermission in lPermissions:                       
                return fn(*args,**kwargs)                         
            raise Exception("permission denied")                  
        return decorated                                          
    return decorator       

def get_permissions(iUserId): #這樣裝飾器就不會拋出NameError
    return ['logged_in',]

def current_user_id():        #名稱錯誤也是如此
    return 1

#如今咱們能夠進行裝飾了                                 

@requires_permission('administrator')
def delete_user(iUserId):
   """
   刪除具備給定Id的用戶,只有具備管理員權限的用戶才能訪問此函數
   """

@requires_permission('logged_in')
def new_game():
    """
    任何已登陸的用戶均可以啓動一個新遊戲
    """

@requires_permission('premium_member')
def premium_checkpoint():
   """
   保存遊戲進程,只容許高級成員訪問
   """

嘗試調用delete_usernew_gamepremium_checkpoint看看會發生什麼。

premium_checkpointdelete_user都在消息「權限被拒絕」的狀況下引起異常,new_game執行得很好(但沒有太多的做用)。

下面是裝飾器的通常形式,帶有參數和使用說明:

def outer_decorator(*outer_args,**outer_kwargs):                            
    def decorator(fn):                                            
        def decorated(*args,**kwargs):                            
            do_something(*outer_args,**outer_kwargs)                      
            return fn(*args,**kwargs)                         
        return decorated                                          
    return decorator       

@outer_decorator(1,2,3)
def foo(a,b,c):
    print (a)
    print (b)
    print (c)

foo()

這至關於:

def decorator(fn):                                            
    def decorated(*args,**kwargs):                            
        do_something(1,2,3)                      
        return fn(*args,**kwargs)                         
    return decorated                                          
return decorator       

@decorator
def foo(a,b,c):
    print (a)
    print (b)
    print (c)

foo()

裝飾課程

裝飾器不只限於對函數進行操做,它們也能夠對類進行操做。比方說,咱們有一個類能夠作不少很是重要的事情,咱們想要把它所作的一切都進行計時。而後咱們可使用time_this像之前同樣使用裝飾器:

class ImportantStuff(object):
    @time_this
    def do_stuff_1(self):
        ...
    @time_this
    def do_stuff_2(self):
        ...
    @time_this
    def do_stuff_3(self):
        ...

這樣就能夠了。可是這個類中還有一些額外的代碼行。若是咱們寫一些更多的類方法並忘記裝飾它們中的一個呢?若是咱們決定再也不爲進行計時怎麼辦?這裏確定存在人爲錯誤的空間。這樣編寫它會好得多:

@time_all_class_methods
class ImportantStuff:
    def do_stuff_1(self):
        ...
    def do_stuff_2(self):
        ...
    def do_stuff_3(self):
        ...

如你所知,該代碼至關於:

class ImportantStuff:
    def do_stuff_1(self):
        ...
    def do_stuff_2(self):
        ...
    def do_stuff_3(self):
        ...

ImportantStuff = time_all_class_methods(ImportantStuff)

那麼time_all_class_methods是如何工做的? 首先,咱們知道它須要將一個類做爲參數,並返回一個類。咱們也知道返回類的函數應該與原始ImportantStuff類的函數相同。也就是說,咱們仍然但願想要完成重要的事情,咱們須要進行計時。如下是咱們將如何作到這一點:

def time_this(original_function):      
    print ("decorating")                      
    def new_function(*args,**kwargs):
        print ("starting timer")      
        import datetime                 
        before = datetime.datetime.now()                     
        x = original_function(*args,**kwargs)                
        after = datetime.datetime.now()                      
        print ("Elapsed Time = {0}".format(after-before))      
        return x                                             
    return new_function  

def time_all_class_methods(Cls):
    class NewCls(object):
        def __init__(self,*args,**kwargs):
            self.oInstance = Cls(*args,**kwargs)
        def __getattribute__(self,s):
            """
            每當訪問NewCls對象的任何屬性時,都會調用這個函數。這個函數首先嚐試
            從NewCls獲取屬性。若是失敗,則嘗試從self獲取屬性。oInstance(一個
            修飾類的實例)。若是它設法從self獲取屬性。oInstance,
            屬性是一個實例方法,而後應用' time_this '。
            """
            try:    
                x = super(NewCls,self).__getattribute__(s)
            except AttributeError:      
                pass
            else:
                return x
            x = self.oInstance.__getattribute__(s)
            if type(x) == type(self.__init__): # 這是一個實例方法
                return time_this(x)                 # 這等價於用time_this修飾方法
            else:
                return x
    return NewCls

#如今讓咱們作一個虛擬類來測試它:

@time_all_class_methods
class Foo(object):
    def a(self):
        print ("entering a")
        import time
        time.sleep(3)
        print ("exiting a")

oF = Foo()
oF.a()

結論

在裝飾器的高級用法中,我向你展現了使用Python裝飾器的一些技巧 - 我已經向你展現瞭如何將參數傳遞給裝飾器,以及如何裝飾類。但這仍然只是冰山的一角。在各類奇怪的狀況下,有大量的方法用於裝飾器。你甚至能夠裝飾你的裝飾器(但若是你到達那一點,那麼作一個全面的檢查多是個好主意)。Python同時內置了一些值得了解的裝飾器,例如裝飾器staticmethodclassmethod

接下來要怎麼作?除了我在這篇文章中向你展現的內容外,一般不須要對裝飾器執行任何更復雜的操做。若是你對更改類功能的更多方法感興趣,那麼我建議閱讀有關繼承和通常OO設計原則的數據。或者,若是你真的想學會他們,那麼請閱讀元類(但一樣,處理這些東西幾乎不須要)。

相關文章
相關標籤/搜索