介紹
首先我要承認,裝飾器非常難!你在本教程中看到的一些代碼將會有一些復雜。大多數人在學習Python時都跟裝飾器做過斗爭,所以如果這對你來說很奇怪,不要感到沮喪,因為同樣的大多數人都可以克服這種苦難。在本教程中,我將逐步介紹了解裝飾器的過程。首先我假設你已經可以編寫基本函數和基本類。如果你不能做這些事,那么我建議你在回到這里之前先學習如何去做到編寫基本函數和基本類(除非你迷路了,在這種情況下你可以原諒)。
用例:計時函數執行
假設我們正在執行一段代碼,執行時間比我們想的還要長一些。這段代碼由一堆函數調用組成,我們確信這些調用中至少有一個調用構成了我們代碼中的瓶頸。我們如何找到瓶頸?現在有一個解決方案,就是我們現在要關注的解決方案,就是對函數執行進行計時。
讓我們從一個簡單的例子開始。我們只有一個函數需要計時,func_a
def func_a(stuff): do_important_things_1() do_important_things_2() do_important_things_3()
一種方法是將時鐘代碼放在每個函數調用周圍。所以就像這樣:
func_a(current_stuff)
看起來會更像這樣:
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))
這種方法的好處是:
- 我們將代碼放在一個地方,所以如果我們想要更改它(例如,如果我們想將經過的時間存儲在數據庫或日志中)那么我們只需要在一個地方而不是每一個函數調用中更改它
- 我們不需要記住每次調用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_a,func_b和func_c中的方法,這種方法不會讓我們到處復制粘貼代碼。
一個簡單的繞道:返回函數的函數
Python是一種非常特殊的語言,因為函數是第一類對象。這意味著一旦函數在作用域中被定義,它就可以傳遞給函數,賦值給變量,甚至從函數返回。這個簡單的事實是使python裝飾器成為可能的原因。查看下面的代碼,看看你是否可以猜出標記為A,B,C和D的行會發生什么。
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_user,new_game和premium_checkpoint看看會發生什么。
premium_checkpoint和delete_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同時內置了一些值得了解的裝飾器,例如裝飾器staticmethod及classmethod。
接下來要怎么做?除了我在這篇文章中向你展示的內容外,通常不需要對裝飾器執行任何更復雜的操作。如果你對更改類功能的更多方法感興趣,那么我建議閱讀有關繼承和一般OO設計原則的數據。或者,如果你真的想學會他們,那么請閱讀元類(但同樣,處理這些東西幾乎不需要)。