Python高階特性(2):Closures、Decorators和functools

熊崽Kevin發表於2014-05-05

Python高階特性(1):Iterators、Generators和itertools

裝飾器(Decorators)

裝飾器是這樣一種設計模式:如果一個類希望新增其他類的一些功能,而不希望通過繼承或是直接修改原始碼實現,那麼可以使用裝飾器模式。簡單來說Python中的裝飾器就是指某些函式或其他可呼叫物件,以函式或類作為可選輸入引數,然後返回函式或類的形式。通過這個在Python2.6版本中被新加入的特性可以用來實現裝飾器設計模式。

順便提一句,在繼續閱讀之前,如果你對Python中的閉包(Closure)概念不清楚,請檢視本文結尾後的附錄,如果沒有閉包的相關概念,很難恰當的理解Python中的裝飾器。

在Python中,裝飾器被用於用@語法糖修辭的函式或類。現在讓我們用一個簡單的裝飾器例子來演示如何做一個函式呼叫日誌記錄器。在這個例子中,裝飾器將時間格式作為輸入引數,在呼叫被這個裝飾器裝飾的函式時列印出函式呼叫的時間。這個裝飾器當你需要手動比較兩個不同演算法或實現的效率時很有用。

來看一個例子,在這裡add1和add2函式被logged修飾,下面給出了一個輸出示例。請注意在這裡時間格式引數是儲存在被返回的裝飾器函式中(decorated_func)。這就是為什麼理解閉包對於理解裝飾器來說很重要的原因。同樣也請注意返回函式的名字是如何被替換為原函式名的,以防萬一如果它還要被使用到,這是為了防止混淆。Python預設可不會這麼做。

如果你足夠細心,你可能會注意到我們對於返回函式的名字__name__有著特別的處理,但對其他的注入__doc__或是__module__則沒有如此。所以如果,在這個例子中add函式有一個doc字串的話,它就會被丟棄。那麼該如何處理呢?我們當然可以像處理__name__那樣對待所有的欄位,不過如果在每個裝飾器內都這麼做的話未免太繁冗了。這就是為何functools模組提供了一個名為wraps的裝飾器的原因,那正是為了處理這種情況。可能在理解裝飾器的過程中會被迷惑,不過當你把裝飾器看成是一個接收函式名作為輸入引數並且返回一個函式,這樣就很好理解了。我們將在下個例子中使用wraps裝飾器而不是手動去處理__name__或其他屬性。

下個例子會有點複雜,我們的任務是將一個函式呼叫的返回結果快取一段時間,輸入引數決定快取時間。傳遞給函式的輸入引數必須是可雜湊的物件,因為我們使用包含呼叫輸入引數的tuple作為第一個引數,第二個引數則為一個frozenset物件,它包含了關鍵詞項kwargs,並且作為cache key。每個函式都會有一個唯一的cache字典儲存在函式的閉包內。

【譯註】set和frozenset為Python的兩種內建集合,其中前者為可變物件(mutable),其元素可以使用add()或remove()進行變更,而後者為不可變物件(imutable)並且是可雜湊的(hashable),在建立之後元素不可變,他可以作為字典的key或是另一個集合的元素。

來看看它的用法。我們使用裝飾器裝飾一個很基本的斐波拉契數生成器。這個cache裝飾器將對程式碼使用備忘錄模式(Memoize Pattern)。請注意fib函式的閉包是如何存放cache字典、一個指向原fib函式的引用、logged引數的值以及timeout引數的最後值的。dump_closure將在文末定義。

Class Decorators

在之前的小節中,我們看了一些函式裝飾器和一些使用的小技巧,接下來我們來看看類裝飾器。類裝飾器將一個class作為輸入引數(Python中的一種類型別物件),並且返回一個修改過的class。

第一個例子是一個簡單的數學問題。當給定一個有序集合P,我們定義PdP的反序集合P(x,y) <-> Pd(x,y),也就是說兩個有序集合的元素順序互為相反的,這在Python中該如何實現?假定一個類定義了__lt__以及__le__或其他方法來實現有序。那麼我們可以通過寫一個類裝飾器來替換這些方法。

下面是將這個裝飾器用以str型別的例子,建立一個名為rstr的新類,使用反字典序(opposite lexicographic)為其順序。

來看一個更復雜的例子。假定我們希望前面所說的logged裝飾器能夠被用於某個類的所有方法。一個方案是在每個類方法上都加上裝飾器。另一個方案是寫一個類裝飾器自動完成這些工作。在動手之前,我將把前例中的logged裝飾器拿出來做一些小改進。首先,它使用functools提供的wraps裝飾器完成固定__name__的工作。第二,一個_logged_decorator屬性被引入(設定為True的布林型變數),用來指示這個方法是否已經被裝飾器裝飾過,因為這個類可能會被繼承而子類也許會繼續使用裝飾器。最後,name_prefix引數被加入用來設定列印的日誌資訊。

好的,讓我們開始寫類裝飾器:

下面是使用方法,注意被繼承的或被重寫的方法是如何處理的。

我們第一個類裝飾器的例子是類的反序方法。一個相似的裝飾器,可以說是相當有用的,實現__lt__、__le__、__gt__、__ge__和__eq__中的一個,能夠實現類的全排序麼?這也就是functools.total_ordering裝飾器所做的工作。詳情請見參考文件

Flask中的一些例子

讓我們來看看Flask中用到的一些有趣的裝飾器。

假定你希望讓某些函式在特定的呼叫時刻輸出警告資訊,例如僅僅在debug模式下。而你又不希望每個函式都加入控制的程式碼,那麼你就能夠使用裝飾器來實現。以下就是Flask的app.py中定義的裝飾器的工作。

來看一個更有趣的例子,這個例子是Flask的route裝飾器,在Flask類中定義。注意到裝飾器可以是類中的一個方法,將self作為第一個引數。完整的程式碼在app.py中。請注意裝飾器簡單的將被裝飾過的函式註冊成為一個URL控制程式碼,這是通過呼叫add_url_rule函式來實現的。

擴充套件閱讀

1. official Python Wiki

2. metaprogramming in Python 3

附錄:閉包

一個函式閉包是一個函式和一個引用集合的組合,這個引用集合指向這個函式被定義的作用域的變數。後者通常指向一個引用環境(referencing environment),這使得函式能夠在它被定義的區域之外執行。在Python中,這個引用環境被儲存在一個cell的tuple中。你能夠通過func_closure或Python 3中的__closure__屬性訪問它。要銘記的一點是引用及是引用,而不是物件的深度拷貝。當然了,對於不可變物件而言,這並不是問題,然而對可變物件(list)這點就必須注意,隨後會有一個例子說明。請注意函式在定義的地方也有__globals__欄位來儲存全域性引用環境。

來看一個簡單的例子:

一個稍複雜的例子。確保明白為什麼會這麼執行。

【譯者】:z.append(3)時,g()內部的引用和z仍然指向一個變數,而z=[1]之後,兩者就不再指向一個變數了。

最後,來看看程式碼中使用到的dump_closure方法的定義。

相關文章