How does it work – with_metaclass

  • 2019 年 11 月 30 日
  • 筆記

我在看源代碼的時候,經常蹦出這一句:How does it work! 竟然有這種操作?本系列文章,試圖剖析代碼中發生的魔法。順便作為自己的閱讀筆記,以作提高。

先簡單介紹下Python中的元類(metaclass)。元類就是創建類的類,對於元類來說,類是它的實例,isinstance(cls, metaclass)將返回True。Python中的所有類,都是type的實例,換句話說,type是元類的基類。使用type創建一個類的方法如下:

Python

>>> type('MyClass', (), {})  <class '__main__.MyClass'>

type接受三個參數,第一個參數是類名稱,第二個參數是繼承的基類的元組,第三個參數是類的命名空間。上例中,我們創建了一個無基類(直接繼承object),無初始命名空間的類MyClass

註:使用type創建的類和使用元類的類,都是新式類

使用元類後,該類將由定義的元類實例化來創建。定義的方法在Python 2與Python 3中有所不同:

Python

# Python 2:  class MyClass(object):      __metaclass__ = MyMeta    # Python 3:  class MyClass(metaclass=MyMeta):      pass

如果你的項目需要兼容Python 2和Python 3,就需要使用一種方法,同時支持Python 2和Python 3。元類有兩個基本特性:

  • 元類實例化得到類
  • 元類能被子類繼承

根據這兩個特性,我們不難得到解決方案:

  • 用元類實例化得到一個臨時類
  • 定義類時繼承這個臨時類

我們可以寫出一個with_metaclass函數:

Python

def with_metaclass(meta, *bases):      """Compatible metaclass        :param meta: the metaclass      :param *bases: base classes      """      return meta('temp_class', bases, {})    # Testing:  class TestMeta(type):      def __new__(cls, name, bases, d):          d['a'] = 'xyz'          return type.__new__(cls, name, bases, d)      class Foo(object):pass    class Bar(with_metaclass(TestMeta, Foo)): pass

我們就創建了一個以TestMeta為元類,繼承Foo的類Bar。驗證:

Python

>>> Bar.a  'xyz'  >>> Bar.__mro__  (<class '__main__.Bar'>, <class '__main__.temp_class'>, <class '__main__.Foo'>, <class 'object'>)

一切正常,但我們看到在Bar的mro里混進了一個臨時類temp_class,你忽略它吧,有時會很麻煩。作為完美主義者,我想尋找一種解決辦法,不要在mro中引入多餘的類。

Python的six模塊專門為解決Python 2to3兼容問題而生,模塊裡帶有一個with_metaclass函數,我們來看它是怎麼實現的:(為了debug,添加了一個print語句)

Python

def with_metaclass(meta, *bases):      class metaclass(type):          def __new__(cls, name, this_bases, d):              print(cls, "new is called")              return meta(name, bases, d)      return type.__new__(metaclass, 'temp_class', (), {})    # Testing:  class TestMeta(type):      def __new__(cls, name, bases, d):          d['a'] = 'xyz'          print(cls, "new is called")          return type.__new__(cls, name, bases, d)

一時看不懂?沒關係,我們來用用看,為了看清楚過程,我們分成兩步執行:

Python

>>> temp = with_metaclass(TestMeta, Foo)  >>> class Bar(temp): pass  ...  <class '__main__.with_metaclass.<locals>.metaclass'> new is called  <class '__main__.TestMeta'> new is called  >>> Bar.a  'xyz'  >>> Bar.__mro__  (<class '__main__.Bar'>, <class '__main__.Foo'>, <class 'object'>)

我們明明生成了一個臨時類temp_class,但後來竟然消失了!下面來仔細分析函數的運行過程。首先我們看到,執行第一步生成臨時類時,兩個__new__都沒有調用,而第二步定義類時,兩個__new__都調用了。奧秘就在函數的返回語句return type.__new__(metaclass, 'temp_class', (), {}),它創建了一個臨時類,具有如下屬性:

  • 名稱為temp_class
  • 是函數內部類metaclass的實例,它的元類是metaclass
  • 沒有基類
  • 創建時僅調用了type__new__的方法

這是一個**metaclass實例的不完全版本**。接下來,定義Bar時,Bar得到繼承的元類metaclass,過程如下:

  1. 實例化metaclass
  2. 調用metaclass.__new__
  3. 返回meta(name, bases, d)meta=TestMetabases=(Foo,)
  4. 調用TestMeta.__new__實例化得到Bar

Bar的基類由第3步得到,於是就去除了temp_class,這其實用到了閉包,with_metaclass返回的臨時類中,本身無任何屬性,但包含了元類和基類的所有信息,並在下一步定義類時將所有信息解包出來。

以上就是with_metaclass源代碼的解析,通過這篇文章,相信能加深元類與閉包的理解。