關于Python
黑魔法 metaclass
的兩種極端觀點:
-
這種特性太牛逼了,是無所不能的阿拉丁神燈,必須找機會用上才能顯示自己的
Python
實力。 - 這個特性太危險,會蠱惑人心去濫用,一旦打開就會釋放惡魔,讓代碼難以維護。
今天我們就來看看,metaclass
到底是阿拉丁神燈,還是潘多拉魔盒。
一、什么是 metaclass
很多書都會翻譯成 元類,僅從字面理解, meta 的確是元,本源,翻譯沒毛病。但理解時,應該把元理解為描述數據的超越數據,事實上,
metaclass
的 meta 起源于希臘詞匯meta
,包含兩種意思:
- “
Beyond
”,例如技術詞匯metadata
,意思是描述數據的超越數據。- “
Change
”,例如技術詞匯metamorphosis
,意思是改變的形態。因此可以理解為
metaclass
為描述類的超類,同時可以改變子類的形態。你可能會問了,這和元數據的定義差不多么,這種特性在編程中有什么用?用處非常大。在沒有
metaclass
的情況下,子類繼承父類,父類是無法對子類執行操作的,但有了metaclass
,就可以對子類進行操作,就像裝飾器那樣可以動態定制和修改被裝飾的類,metaclass
可以動態的定制或修改繼承它的子類。
二、metaclass 能解決什么問題?
你已經知道了
metaclass
可以像裝飾器那樣定制和修改繼承它的子類,這里就說下它能解決什么實際問題。比方說,在一個智能語音助手的大型項目中,我們有 1 萬個語音對話場景,每一個場景都是不同團隊開發的。作為智能語音助手的核心團隊成員,你不可能去了解每個子場景的實現細節。在動態配置實驗不同場景時,經常是今天要實驗場景 A 和 B 的配置,明天實驗 B 和 C 的配置,光配置文件就有幾萬行量級,工作量不可謂不小。而應用這樣的動態配置理念,我就可以讓引擎根據我的文本配置文件,動態加載所需要的
Python
類。如果你還不是很清楚,那么
YAML
你應該知道,它是一個家喻戶曉的 Python 工具,可以方便地序列化和反序列化數據,YAMLObject
可以讓它的任意子類支持序列化和反序列化(serialization & deserialization
)。序列化和反序列化:
- 序列化:當程序運行時,所有的變量或者對象都是存儲到內存中的,一旦程序調用完成,這些變量或者對象所占有的內存都會被回收。而為了實現變量和對象持久化的存儲到磁盤中或在網絡上進行傳輸,我們需要將變量或者對象轉化為二進制流的方式。而將其轉化為二進制流的過程就是序列化。
- 反序列化:而反序列化就是說程序運行的時候不能從磁盤中進行讀取,需要將序列化的對象或者變量從磁盤中轉移到內存中,同時也會將二進制流轉換為原來的數據格式。我們把這一過程叫做反序列化。
現在你有 1 萬個不同格式的 YAML 配置文件,本來你需要寫 1 萬個類來加載這些配置文件,有了
metaclass
,你只需要實現一個metaclass
超類,然后再實現一個子類繼承這個metaclass
,就可以根據不同的配置文件自動拉取不同的類,這極大地提高了效率。
三、通過一個實例來理解 metaclass
請手動在 ipython
中搞代碼,看看每一步都輸出了什么,這樣可以徹底的理解類的創建和實例化步驟。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
In[ 15 ]: class Mymeta( type ): ...: def __init__( self , name, bases, dic): ...: super ().__init__(name, bases, dic) ...: print ( '===>Mymeta.__init__' ) ...: print ( self .__name__) ...: print (dic) ...: print ( self .yaml_tag) ...: ...: def __new__( cls , * args, * * kwargs): ...: print ( '===>Mymeta.__new__' ) ...: print ( cls .__name__) ...: return type .__new__( cls , * args, * * kwargs) ...: ...: def __call__( cls , * args, * * kwargs): ...: print ( '===>Mymeta.__call__' ) ...: obj = cls .__new__( cls ) ...: cls .__init__( cls , * args, * * kwargs) ...: return obj ...: In[ 16 ]: In[ 16 ]: In[ 16 ]: class Foo(metaclass = Mymeta): ...: yaml_tag = '!Foo' ...: ...: def __init__( self , name): ...: print ( 'Foo.__init__' ) ...: self .name = name ...: ...: def __new__( cls , * args, * * kwargs): ...: print ( 'Foo.__new__' ) ...: return object .__new__( cls ) ...: = = = >Mymeta.__new__ Mymeta = = = >Mymeta.__init__ Foo { '__module__' : '__main__' , '__qualname__' : 'Foo' , 'yaml_tag' : '!Foo' , '__init__' : <function Foo.__init__ at 0x0000000007EF3828 >, '__new__' : <function Foo.__new__ at 0x0000000007EF3558 >} !Foo In[ 17 ]: foo = Foo( 'foo' ) = = = >Mymeta.__call__ Foo.__new__ Foo.__init__ In[ 18 ]: |
從上面的運行結果可以發現在定義 class Foo()
定義時,會依次調用 MyMeta
的 __new__
和 __init__
方法構建 Foo
類,然后在調用 foo = Foo()
創建類的實例對象時,才會調用 MyMeta 的 __call__
方法來調用 Foo
類的 __new__
和 __init__ 方
法。
把上面的例子運行完之后就會明白很多了,正常情況下我們在父類中是不能對子類的屬性進行操作,但是元類可以。換種方式理解:元類、裝飾器、類裝飾器都可以歸為元編程。
四、Python 底層語言設計層面是如何實現 metaclass 的?
要理解 metaclass
的底層原理,你需要深入理解 Python
類型模型。下面,將分三點來說明。
1、所有的 Python 的用戶定義類,都是 type 這個類的實例。
可能會讓你驚訝,事實上,類本身不過是一個名為 type 類的實例。在 Python 的類型世界里,type 這個類就是造物的上帝。這可以在代碼中驗證:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
In [ 2 ]: # Python 3和Python 2類似 ...: class MyClass: ...: pass ...: ...: instance = MyClass() ...: in [ 3 ]: type (instance) ...: Out[ 2 ]: __main__.MyClass In [ 4 ]: type (MyClass) ...: Out[ 4 ]: type In [ 5 ]: |
你可以看到,instance
是 MyClass
的實例,而 MyClass 不過是“上帝” type
的實例。
2、用戶自定義類,只不過是 type 類的 __call__ 運算符重載
當我們定義一個類的語句結束時,真正發生的情況,是 Python 調用 type 的 __call__
運算符。簡單來說,當你定義一個類時,寫成下面這樣時:
1
2
|
class MyClass: data = 1 |
Python 真正執行的是下面這段代碼:
1
|
class = type (classname, superclasses, attributedict) |
這里等號右邊的 type(classname, superclasses, attributedict),
就是 type 的 __call__
運算符重載,它會進一步調用:
1
2
|
type .__new__(typeclass, classname, superclasses, attributedict) type .__init__( class , classname, superclasses, attributedict) |
當然,這一切都可以通過代碼驗證,比如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
In [ 5 ]: class MyClass: ...: data = 1 ...: ...: instance = MyClass() ...: In [ 6 ]: MyClass, instance ...: Out[ 6 ]: (__main__.MyClass, <__main__.MyClass at 0x4ef5188 >) In [ 7 ]: instance.data ...: Out[ 7 ]: 1 In [ 8 ]: MyClass = type ( 'MyClass' , (), { 'data' : 1 }) ...: instance = MyClass() ...: In [ 9 ]: MyClass, instance ...: Out[ 9 ]: (__main__.MyClass, <__main__.MyClass at 0x4f40748 >) In [ 10 ]: instance.data ...: Out[ 10 ]: 1 In [ 11 ]: |
由此可見,正常的 MyClass
定義,和你手工去調用 type
運算符的結果是完全一樣的。
3、,“超越變形”正常的類
metaclass 是 type 的子類,通過替換 type 的 __call__
運算符重載機制,“超越變形”正常的類
其實,理解了以上幾點,我們就會明白,正是 Python 的類創建機制,給了 metaclass 大展身手的機會。
一旦你把一個類型 MyClass
的 metaclass
設置成 MyMeta,MyClass 就不再由原生的 type 創建,而是會調用 MyMeta
的 __call__
運算符重載。
1
2
3
|
class = type (classname, superclasses, attributedict) # 變為了 class = MyMeta(classname, superclasses, attributedict) |
四、使用 metaclass 的風險
不過,凡事有利必有弊,尤其是
metaclass
這樣“逆天”的存在。正如你所看到的那樣,metaclass
會"扭曲變形"正常的 Python 類型模型。所以,如果使用不慎,對于整個代碼庫造成的風險是不可估量的。換句話說,
metaclass
僅僅是給小部分Python
開發者,在開發框架層面的Python
庫時使用的。而在應用層,metaclass
往往不是很好的選擇。
總結:
本文從 Python 類創建的過程,幫助你理解 metaclass
的作用。
metaclass
是黑魔法,使用得當就是天堂,反之就是地獄。
到此這篇關于Python黑魔法之metaclass詳情的文章就介紹到這了,更多相關Python黑魔法之metaclass內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://mp.weixin.qq.com/s?__biz=MzU0OTg3NzU2NA==&mid=2247484465&idx=1&sn=7c564ead74b9c62a776009cb61e55f44&scene=21#wechat_redirect