簡單地理解 Python 的裝飾器

發表於2017-09-07

Python有大量強大又貼心的特性,如果要列個最受歡迎排行榜,那麼裝飾器絕對會在其中。

剛接觸裝飾器,會覺得程式碼不多卻難以理解。其實裝飾器的語法本身挺簡單的,複雜是因為同時混雜了其它的概念。下面我們一起拋去無關概念,簡單地理解下Python的裝飾器。

裝飾器的原理

在直譯器下跑個裝飾器的例子,直觀地感受一下。

make_bold裝飾的get_content,呼叫後返回結果會自動被b標籤包住。怎麼做到的呢,簡單4步就能明白了。

1. 函式是物件

我們定義個get_content函式。這時get_content也是個物件,它能做所有物件的操作。

它有id,有type,有值。

跟其他物件一樣可以被賦值給其它變數。

它可以當引數傳遞,也可以當返回值

2. 自定義函式物件

我們可以用class來建構函式物件。有成員函式__call__的就是函式物件了,函式物件被呼叫時正是呼叫的__call__

我們來呼叫看看。可以看到,函式物件的使用分兩步:構造和呼叫(同學們注意了,這是考點)。

3. @是個語法糖

裝飾器的@沒有做什麼特別的事,不用它也可以實現一樣的功能,只不過需要更多的程式碼。

make_bold是個函式,要求入參是函式物件,返回值是函式物件。@的語法糖其實是省去了上面最後一行程式碼,使可讀性更好。用了裝飾器後,每次呼叫get_content,真正呼叫的是make_bold返回的函式物件。

4. 用類實現裝飾器

入參是函式物件,返回是函式物件,如果第2步裡的類的建構函式改成入參是個函式物件,不就正好符合要求嗎?我們來試試實現make_bold

大功告成,看看能不能用。

成功實現裝飾器!是不是很簡單?

這裡分析一下之前強調的構造呼叫兩個過程。我們去掉@語法糖好理解一些。

到這裡就徹底清楚了,完結撒花,可以關掉網頁了~~~(如果只是想知道裝飾器原理的話)

函式版裝飾器

閱讀原始碼時,經常見到用巢狀函式實現的裝飾器,怎麼理解?同樣僅需4步。

1. def的函式物件初始化

class實現的函式物件很容易看到什麼時候構造的,那def定義的函式物件什麼時候構造的呢?

不像一些編譯型語言,程式在啟動時函式已經構造那好了。上面的例子可以看到,執行到def會才構造出一個函式物件,並賦值給變數make_bold

這段程式碼和下面的程式碼效果是很像的。

2. 巢狀函式

Python的函式可以巢狀定義。

inner是在outer內定義的,所以算outer的區域性變數。執行到def inner時函式物件才建立,因此每次呼叫outer都會建立一個新的inner。下面可以看出,每次返回的inner是不同的。

3. 閉包

巢狀函式有什麼特別之處?因為有閉包。

下面的試驗表明,inner可以訪問到outer的區域性變數msg

閉包有2個特點

  1. inner能訪問outer及其祖先函式的名稱空間內的變數(區域性變數,函式引數)。
  2. 呼叫outer已經返回了,但是它的名稱空間被返回的inner物件引用,所以還不會被回收。

這部分想深入可以去了解Python的LEGB規則。

4. 用函式實現裝飾器

裝飾器要求入參是函式物件,返回值是函式物件,巢狀函式完全能勝任。

用法跟類實現的裝飾器一樣。可以去掉@語法糖分析下構造呼叫的時機。

因為返回的wrapper還在引用著,所以存在於make_bold名稱空間的func不會消失。make_bold可以裝飾多個函式,wrapper不會呼叫混淆,因為每次呼叫make_bold,都會有建立新的名稱空間和新的wrapper

到此函式實現裝飾器也理清楚了,完結撒花,可以關掉網頁了~~~(後面是使用裝飾的常見問題)

常見問題

1. 怎麼實現帶引數的裝飾器?

帶引數的裝飾器,有時會異常的好用。我們看個例子。

怎麼做到的呢?其實這跟裝飾器語法沒什麼關係。去掉@語法糖會變得很容易理解。

上面程式碼中的unnamed_decorator才是真正的裝飾器,make_header是個普通的函式,它的返回值是裝飾器。

來看一下實現的程式碼。

看了實現程式碼,裝飾器的構造呼叫的時序已經很清楚了。

2. 如何裝飾有引數的函式?

為了有條理地理解裝飾器,之前例子裡的被裝飾函式有意設計成無參的。我們來看個例子。

最直接的想法是把get_login_tip的引數透傳下去。

如果被裝飾的函式引數是明確固定的,這麼寫是沒有問題的。但是make_bold明顯不是這種場景。它既需要裝飾沒有引數的get_content,又需要裝飾有引數的get_login_tip。這時候就需要可變引數了。

當裝飾器不關心被裝飾函式的引數,或是被裝飾函式的引數多種多樣的時候,可變引數非常合適。可變引數不屬於裝飾器的語法內容,這裡就不深入探討了。

3. 一個函式能否被多個裝飾器裝飾?

下面這麼寫合法嗎?

合法。上面的的程式碼和下面等價,留意一下裝飾的順序。

4. functools.wraps有什麼用?

Python的裝飾器倍感貼心的地方是對呼叫方透明。呼叫方完全不知道也不需要知道呼叫的函式被裝飾了。這樣我們就能在呼叫方的程式碼完全不改動的前提下,給函式patch功能。

為了對呼叫方透明,裝飾器返回的物件要偽裝成被裝飾的函式。偽裝得越像,對呼叫方來說差異越小。有時光偽裝函式名和引數是不夠的,因為Python的函式物件有一些元資訊呼叫方可能讀取了。為了連這些元資訊也偽裝上,functools.wraps出場了。它能用於把被呼叫函式的__module____name____qualname____doc____annotations__賦值給裝飾器返回的函式物件。

對比一下效果。

實現裝飾器時往往不知道呼叫方會怎麼用,所以養成好習慣加上functools.wraps吧。

這次是真•完結了,有疑問請留言,撒花吧~~~

相關文章