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
,過程如下:
- 實例化
metaclass
- 調用
metaclass.__new__
- 返回
meta(name, bases, d)
,meta=TestMeta
,bases=(Foo,)
- 調用
TestMeta.__new__
實例化得到Bar
Bar
的基類由第3步得到,於是就去除了temp_class
,這其實用到了閉包,with_metaclass
返回的臨時類中,本身無任何屬性,但包含了元類和基類的所有信息,並在下一步定義類時將所有信息解包出來。
以上就是with_metaclass
源代碼的解析,通過這篇文章,相信能加深元類與閉包的理解。