Python: 陌生的 metaclass

發表於2016-11-17

元類

Python 中的元類(metaclass)是一個深度魔法,平時我們可能比較少接觸到元類,本文將通過一些簡單的例子來理解這個魔法。

類也是物件

在 Python 中,一切皆物件。字串,列表,字典,函式是物件,類也是一個物件,因此你可以:

  • 把類賦值給一個變數
  • 把類作為函式引數進行傳遞
  • 把類作為函式的返回值
  • 在執行時動態地建立類

看一個簡單的例子:

熟悉又陌生的 type

在日常使用中,我們經常使用 object 來派生一個類,事實上,在這種情況下,Python 直譯器會呼叫 type 來建立類。

這裡,出現了 type,沒錯,是你知道的 type,我們經常使用它來判斷一個物件的型別,比如:

事實上,type 除了可以返回物件的型別,它還可以被用來動態地建立類(物件)。下面,我們看幾個例子,來消化一下這句話。

使用 type 來建立類(物件)的方式如下:

type(類名, 父類的元組(針對繼承的情況,可以為空),包含屬性和方法的字典(名稱和值))

最簡單的情況

假設有下面的類:

現在,我們不使用 class 關鍵字來定義,而使用 type,如下:

上面兩種方式是等價的。我們看到,type 接收三個引數:

  • 第 1 個引數是字串 ‘Foo’,表示類名
  • 第 2 個引數是元組 (object, ),表示所有的父類
  • 第 3 個引數是字典,這裡是一個空字典,表示沒有定義屬性和方法

在上面,我們使用 type() 建立了一個名為 Foo 的類,然後把它賦給了變數 Foo,我們當然可以把它賦給其他變數,但是,此刻沒必要給自己找麻煩。

接著,我們看看使用:

有屬性和方法的情況

假設有下面的類:

type 來建立這個類,如下:

上面兩種方式的效果是一樣的,看下使用:

繼承的情況

再來看看繼承的情況,假設有如下的父類:

我們用 Base 派生一個 Foo 類,如下:

改用 type 來建立,如下:

什麼是元類(metaclass)

元類(metaclass)是用來建立類(物件)的可呼叫物件。這裡的可呼叫物件可以是函式或者類等。但一般情況下,我們使用類作為元類。對於例項物件、類和元類,我們可以用下面的圖來描述:

我們在前面使用了 type 來建立類(物件),事實上,type 就是一個元類

那麼,元類到底有什麼用呢?要你何用…

元類的主要目的是為了控制類的建立行為。我們還是先來看看一些例子,以消化這句話。

元類的使用

先從一個簡單的例子開始,假設有下面的類:

現在我們想給這個類的方法和屬性名稱前面加上 my_ 字首,即 name 變成 my_name,bar 變成 my_bar,另外,我們還想加一個 echo 方法。當然,有很多種做法,這裡展示用元類的做法。

1.首先,定義一個元類,按照預設習慣,類名以 Metaclass 結尾,程式碼如下:

上面的程式碼有幾個需要注意的點:

  • PrefixMetaClass 從 type 繼承,這是因為 PrefixMetaclass 是用來建立類的
  • __new__ 是在 __init__ 之前被呼叫的特殊方法,它用來建立物件並返回建立後的物件,對它的引數解釋如下:
    • cls:當前準備建立的類
    • name:類的名字
    • bases:類的父類集合
    • attrs:類的屬性和方法,是一個字典

2.接著,我們需要指示 Foo 使用 PrefixMetaclass 來定製類。

在 Python2 中,我們只需在 Foo 中加一個 __metaclass__ 的屬性,如下:

在 Python3 中,這樣做:

現在,讓我們看看使用:

可以看到,Foo 原來的屬性 name 已經變成了 my_name,而方法 bar 也變成了 my_bar,這就是元類的魔法。

再來看一個繼承的例子,下面是完整的程式碼:

其中,PrefixMetaclass 和 Foo 跟前面的定義是一樣的,只是新增了 Bar,它繼承自 Foo。先讓我們看看使用:

我們發現,Bar 沒有 prop 這個屬性,但是有 my_prop 這個屬性,這是為什麼呢?

原來,當我們定義 class Bar(Foo) 時,Python 會首先在當前類,即 Bar 中尋找 __metaclass__,如果沒有找到,就會在父類 Foo 中尋找 __metaclass__,如果找不到,就繼續在 Foo 的父類尋找,如此繼續下去,如果在任何父類都找不到 __metaclass__,就會到模組層次中尋找,如果還是找不到,就會用 type 來建立這個類。

這裡,我們在 Foo 找到了 __metaclass__,Python 會使用 PrefixMetaclass 來建立 Bar,也就是說,元類會隱式地繼承到子類,雖然沒有顯示地在子類使用 __metaclass__,這也解釋了為什麼 Bar 的 prop 屬性被動態修改成了 my_prop。

寫到這裡,不知道你理解元類了沒?希望理解了,如果沒理解,就多看幾遍吧~

小結

  • 在 Python 中,類也是一個物件。
  • 類建立例項,元類建立類。
  • 當你建立類時,直譯器會呼叫元類來生成它,定義一個繼承自 object 的普通類意味著呼叫 type 來建立它。

參考資料

相關文章