Python黑魔法之metaclass詳情

關於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 中搞代碼,看看每一步都輸出瞭什麼,這樣可以徹底的理解類的創建和實例化步驟。

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 這個類就是造物的上帝。這可以在代碼中驗證:

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__ 運算符。簡單來說,當你定義一個類時,寫成下面這樣時:

class MyClass:
    data = 1


Python 真正執行的是下面這段代碼:

class = type(classname, superclasses, attributedict)


這裡等號右邊的 type(classname, superclasses, attributedict),就是 type 的 __call__ 運算符重載,它會進一步調用:

type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)

當然,這一切都可以通過代碼驗證,比如

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__ 運算符重載。

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內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: