在Python中,只要在某些元素加上@,就能實現一些魔法,而初學Python時最先接觸的,應該是@staticmethod與@classmethod,剛開始,會以為這是Python的內建語法之一,實際上,這是Python的裝飾器(Decorators)實現。然而,在PEP318中就提過了,裝飾器這名稱容易令人誤會,因為它跟Gof設計模式中的裝飾器定義並不一致,而且,它能做到的功能更多。

從高階函式開始

在Python中,函式是一級值(first-class value)。就現今來說,這個事實會令許多開發者聯想到:這類的函式在許多場合進行傳遞時,會得到許多的效益,像是單純地將程式碼傳遞至另一函式之中,以實現程式碼的共用樣板封裝、建立閉包(Closure)來保存執行情境(Context),或者是從函式中傳回函式等作法;更進階一點的,有些開發者可能就會聯想到函數式程式設計。

在函數式程式設計中,如果有個函式可以接受函式,進行若干處理之後傳回函式,那麼,會稱呼這個函式為高階函式(High-order function),在Python中,裝飾器語法會被加入的動機,正是從這個需求而來,像是PEP318中的例子:

def foo(self):
  # 做一些操作後傳回函式>
foo = classmethod(foo)

classmethod本質上是一個函式,它接受foo參考的函式,進行一些操作,然後,傳回函式並指定給foo參考,而當這類的函式轉換變多時,或者需要額外的引數,例如foo = synchronized(lock)(foo),我們馬上會發現程式的可讀性迅速降低。

因此,Python 2.4之後引入了裝飾器,如果有個函式可接受函式,並傳回函式時,可直接使用@來進行標註:

@classmethod
def foo(self):
  # 做一些操作後傳回函式

這實際上是個語法蜜糖,效果等同於foo = classmethod(foo),而foo = synchronized(lock)(foo)的情況,則可以在foo上,標註@synchronized(lock),此時,只要搭配適當的命名,程式碼就會有良好的可讀性。

然而,重點在於,classmethod這類函式中進行的魔法,可以在不修改被標註對象的程式碼下,擴充它的功能,甚至是完全改變它的行為。

只是裝飾器?

就許多運用Python的場合來說,實際上,不太需要自訂裝飾器,標準程式庫就內建了許多實作,像是@staticmethod、@classmethod、@property、@abstractmethod、@total_ordering等。

如果標準程式庫所內建的實作還不夠,還有PythonDecoratorLibrary這類第三方程式庫實現。

如果非得自己實現一個裝飾器,有一個基本的架構是:

def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwds):
        # f(*args, **kwds)前、後做一些操作
    return wrapper

當被裝飾的函式受到呼叫時,等同於呼叫傳回的wrapper函式,由於實際上my_decorator傳回了另一個函式,函式上的基本資訊已經不等同於傳入的f函式,這會造成一些除錯上的困擾,因此建議使用funtools模組的@wrap來裝飾f,這會將f的模組(__module__)、函式名稱(__name__)、docStrings(__doc__)等複製給wrapper,而@wrap又是一個裝飾器。

而在常見的自訂裝飾器範例當中,基本上,都是類似的架構,通常也確實是在f(*args, **kwds)呼叫前後做些處理(像是@log做些日誌處理),符合Gof中裝飾器的概念。

不過,Python中有些特性一旦結合裝飾器來使用的話,就又不像是Gof裝飾器了。

相關的應用範例之一,是使用了標準程式庫中的@contextmanager,從而自訂情境管理器(Context manager),以便搭配with as進行情境管理,由於搭配了yield建立了產生器,以便重用類似情境的樣板程式碼(像是檔案的關閉、資料庫的commit、rollback等),操作起來,又像是Template callback模式的實現。

而像@property可以搭配描述器(Descriptor),實現攔截屬性的存取,@abstractmethod的實現,本身會動態繼承、建立一個新類別,而@total_ordering的實現,本身會修改被裝飾的類別,顯然地,當中能做的事情,又遠超過Gof裝飾器的職責,進入了像是AOP(Aspect-oriented programming),甚至是meta-programming的範疇。

更像是巨集

儘管一些歷史性的原因,Python的裝飾器使用了@這個符號,這讓人聯想到javadoc或者是Java的標註(Annotation),而Java的標註確實也應用在AOP,以及有限的meta-programming上,不過,實際在PEP318中指出,Python的裝飾器在許多運用上,也許更接近於編譯器的領域,而在Bruce Eckel的〈Decorators I: Introduction to Python Decorators〉中一開頭,就特別指出:「與Python裝飾器最接近的,我想是巨集(Macros)了」。

談到巨集,接觸過C的開發者,馬上會想到C的巨集前置處理指令。Bruce Eckel在文件中,指出:「在一門語言中,巨集的目的是提供一個修改語言元素的方式」,而Python的裝飾器可以修改函式、方法或類別,而且使用的是Python本身的語法,而不像C的巨集使用的是另一套語言。

由於meta-programming本身並沒有嚴謹的定義,在我先前專欄〈動態擴展語言元素的程式設計〉也就談過,像是C的巨集,也可視為meta-programming的一種形式。

而相對於Java 5之後的自訂標註來說,雖然將標註納入了Java語法本身,不過想要實現修改語言元素這個目的,還得透過反射之類的機制來達成,結果就是囉嗦,而且困難重重,就某些程度來說,AOP這名詞就是因此而創造出來的。正如我先前專欄〈從攔截過濾器到AOP〉中,所談到的:「在(Python)這類語言中,識別、分離並實現橫切關注,相對來說是件稀鬆平常之事。」

使用語言本身去處理語言

因此在Python中,Python裝飾器也不是單純用來更簡單地實現AOP,而是使用語言本身去處理語言,這讓人想到Ruby或Groovy這類語言,可以建立內部DSL,甚至實現一個建構系統,例如,在〈Python Decorators III: A Decorator-Based Build System〉中,就示範了相關的作法,解釋如何使用Python裝飾器,實現一個Python建構(Build)系統。

實際上,如果想要在Python中,有個巨集之類的功能,也可以透過內建的ast模組來實現,像是MacroPy就試著在import時期,對Python程式碼進行轉換處理,可簡單地讓Python程式碼擁有Case Classes、Quick Lambdas之類的擴充語法。

有機會的話,可以看看Python標準程式庫中,@wrap、@contextmanager、@total_ordering等的實現,從這些內建的裝飾器出發,瞭解更多神奇魔法是如何實現的,就能更有彈性地去思考Python裝飾器本身的可能性,而不單只是被裝飾器這名詞給限制住了。

作者簡介


Advertisement

更多 iThome相關內容