Python元類再談

發表於2016-09-01

在Python中一切都是物件,型別也是物件;類比型別和例項的概念,型別也必然有自己的型別,十分合理。事實上,型別的型別其實就是術語元型別的意思,python裡面所有型別的元型別都是type。預設情況下我們新建一個類,在不手動指定元型別的前提下,type會被指定為元型別 ,元型別能夠控制型別的建立和初始化。

一般情況下我們能夠通過關鍵字class來定義一個新的自定義型別,但也能夠通過type動態的生成一個新的型別,下面的兩種實現方式等價:

藉助這個例子,我們還能順便看一下一些關於預設元類type的資訊:

新定義一個型別,當型別被解析的時候(比如當作模組引入),元類會負責建立和初始化這個新的型別,背後的邏輯基本上包括:

  • 執行類的定義程式碼收集所有的屬性和方法
  • 查詢類的元類
  • 執行 Metaclass(name, bases, property_dict),引數分別新建的類的名稱,類的父類tuple,收集的屬性字典
  • 型別建立完成
  • 執行初始化

在上面描述的過程中,自定義指定元類,然後重寫元類的__new____init__方法,因為在指定元類的情況下,除去收集資訊的過程,型別的建立和初始化兩個步驟:

注意這裡的表示方式是呼叫內部方法來表示的,一般來說在重寫的new或者init方法的最後都會呼叫type相應的方法來完成最終的型別建立或初始化工作,這時候也可以使用super關鍵字動態繫結,或者通過__dict__屬性字典訪問是一樣的:

對於__init__這樣的method來說還可以這樣呼叫:

插個題外話,這三種方式的使用其實涉及到python的描述符,使用super和get的時候會進行型別或者例項的繫結,相比直接呼叫內部方法__new____init__,由於繫結了self/cls上下文,在傳遞引數的時候就只用指定除上下文之後的引數了。

從網上搜羅了一個例子,將元類建立和初始化新的型別的過程完整展示出來了:

建立和初始化的過程只會發生一此,也就是會是說 __new__, __init__只會被執行一次,並且在執行完之前,型別MyKlass其實並沒有生成,直接通過名稱訪問會報錯:

在使用元類的過程中,有時候我們會重寫他的__call__方法,這個方法的作用其實和__new__有點相似,只不過這次是控制型別例項物件的生成,因為這個方法恰好和生成型別例項時呼叫的構造方法吻合。關於重寫這個call方法的使用場景,一個比較常用的就是實現單例模式:

例子很直接,__call__方法裡面通過判斷是否已經有初始化過的例項,沒有就仿照正常未指定元類的情況下呼叫type的__call__方法(當然這裡要麼通過super binding要麼手動指定cls上下文),生成一個Foo的例項儲存和返回出來。但是有一個注意點是,call方法每次初始化例項物件的時候都會被呼叫,這也和先前說的控制例項的生成一致:

還有一個需要在意的地方是最後的兩行列印日誌,Foo型別的元類是Metasingleton(呼叫new生成型別的時候預設指定元類是第一個引數);Foo的__call__方法是繫結了Foo(MetaSingleton的例項)例項的MetaSingleton的方法,也就是從另外的方面證實每次初始化Foo型別市裡的時候,其實是在呼叫元類中重寫的__call__方法。

元類這個特性大多數情況下確實使用的不多並且需要稍微花點時間來理解,但是需要使用的時候會非常好用,往往能夠實現很多優雅的功能,最典型的就是ORM的實現了,只不過會更加的複雜,且pythoning且學習吧, PEACE!

參考資料中的連結裡面有有幾個實際的例子,本文也是學習其中的內容後配合一些其它一些使用經驗以及碰到的問題理而成,希望對大家有用。

參考資料:

Python metaclasses by example

Descriptor HowTo Guide

相關文章