Python Decorator的來龍

發表於2016-04-02

引言
本文主要梳理了Python decorator的實現思路,解釋了為什麼Python decorator是現在這個樣子。

關於代理模式、裝飾模式

設計模式中經常提到的代理模式、裝飾模式,這兩種叫法實際上是說的同一件事,只是側重點有所不同而已。

這兩者都是通過在原有物件的基礎上封裝一層物件,通過呼叫封裝後的物件而不是原來的物件來實現代理/裝飾的目的

例如:(以Java為例)

在這個例子中CountProxy是對CountImpl的封裝。
使用者通過CountProxy.queryCount方法來呼叫CountImpl.queryCount方法,這被稱為代理,即CountProxy是代理類,CountImpl是被代理類。
CountProxy.queryCount方法中,可以在CountImpl.queryCount方法呼叫之前和之後新增一些額外的操作,被稱為裝飾,即CountProxy是裝飾類,CountImpl是被裝飾類。

如果強調通過CountProxyCountImpl進行代理的作用,則稱為代理模式;
如果強調通過CountProxyCountImpl增加額外的操作,則稱為裝飾模式;

不論是哪種稱呼,其本質都在於對原有物件的封裝。
其封裝的目的在於增強所封裝物件的功能或管理所封裝的物件。

從上面的例子也可以發現,代理/封裝所圍繞的核心是可呼叫物件(比如函式)。

Python中的代理/裝飾

Python中的可呼叫物件包括函式、方法、實現了__call__方法的類。
Python中的函式也是物件,可以作為高階函式的引數傳入或返回值返回。
因此,當代理/裝飾的物件是函式時,可以使用高階函式來對某個函式進行封裝。
例如:

但是,這個例子中,query_count函式作為引數傳入query_count_proxy函式中,並在query_count_proxy函式中被呼叫,其結果作為返回值返回。這就完成了代理的功能,同時,在呼叫query_count函式的前後,我們還增加了裝飾程式碼。
但是,query_count_proxy的函式引數與query_count不一樣了,理想的代理應該保持介面一致才對。

為了保持一致,我們可以利用高階函式可以返回函式的特點來完成:

修改後的例子,query_count_proxy僅負責接受被代理的函式query_count作為引數,同時,返回一個函式物件wrapper作為返回值,真正的封裝動作在wrapper這個函式中完成。

此時,如果呼叫query_count_proxy(query_count)就得到了wrapper函式物件,則,執行query_count_proxy(query_count)('Lee', 20)就相當於執行了wrapper('Lee', 20)

但是可以看到,query_count_proxy(query_count)('Lee', 20)這種使用方法,仍然不能保證一致。

為了保持一致,我們需要利用Python中物件與其名稱可以動態繫結的特點。
不使用query_count_proxy(quer_count)('Lee', 20)來呼叫代理函式,而是使用下面兩句:

執行query_count_proxy(query_count)生成wrapper函式物件,將這個物件通過query_count = query_count_proxy(query_count)繫結到query_count這個名字上來,這樣執行query_count('Lee', 20)時,其實執行的是wrapper('Lee', 20)

這麼做的結果就是:使用代理時呼叫query_count('Lee', 20)與不使用代理時呼叫query_count('Lee', 20)對使用者而言保持不變,不用改變程式碼,但是在真正執行時,使用的是代理/裝飾後的函式。

這裡,基本利用Python的高階函式及名稱繫結完成了代理/裝飾的功能。
還有什麼不理想的地方呢?
對,就是query_count = query_count_proxy(query_count),因為這句既不簡潔,又屬於重複工作。
Python為我們提供了語法糖來完成這類的tedious work。
方法就是:

query_count = query_count_proxy(query_count)就等同於在定義query_count函式的時候,在其前面加上@query_count_proxy

Python看到這樣的語法,就會自動的執行query_count = query_count_proxy(query_count)進行name rebinding

補充

以上就是Python實現可呼叫物件裝飾的核心。
可呼叫物件包括函式、方法、實現了__call__方法的類,上述內容只是針對函式來解釋,對於方法、實現了__call__方法的類,其基本原理相同,具體實現略有差別。

本文系作者原創,如有轉載請註明出處。
由於水平精力有限,如有錯誤歡迎指正。

相關文章