當前位置:
首頁 > 知識 > Python黑魔法:元類

Python黑魔法:元類


術語「元編程」指的是程序具有編寫或操縱其自身作為它們資料的潛力。Python支持稱為元類的類的元編程。


元類是一個深奧的面向對象編程(OOP)概念,隱藏在幾乎所有的Python代碼之後。無論你是否意識到它的存在,你都一直在使用它們。大多數情況下,你並不需要了解它。而且大多數Python程序員也很少用到,但是某些情況下你就不得不考慮使用元類。


當你有需要時,Python提供了一種不是所有面向對象語言都支持的功能:你可以深入了解其內部並自定義元類。使用定製元類經常會存在爭議,正如Python大咖,創作了Python之禪的蒂姆·彼得斯所言:


「元類比99%的用戶所憂慮的東西具有更深的魔法。如果你猶豫考慮是否需要它們,那麼實質上你不會需要它們(實際需要它們的人確信他們確實需要,並且不需要進行任何解釋)。「    —— 蒂姆·彼得斯

眾多Pythonistas(即Python發燒友所熟知的Python大咖)認為你永遠不應該使用自定義元類。這樣說可能會有點極端,但大部分情況下自定義元類並不是必需的。如果一個問題不是很明顯是否需要它們,那麼如果以一種更簡單的方式解決問題,代碼可能會更乾淨,更具有可讀性。


儘管如此,理解Python元類還是很有必要,因為它可以更好地理解Python類的內部實現。你永遠不知道:你可能有一天會發現自己處於這樣一種情況,即你確切明白自定義元類就是你想要的。







復仇者聯盟3:無限戰爭


主演:小羅伯特·唐尼 / 克里斯·海姆斯沃斯 / 馬克·魯法洛



貓眼電影演出

廣告

購買

舊式類VS新式類


在Python範疇,一個類可以是兩種類型之一。官方術語並沒有對此進行確認,所以它們被非正式地稱為舊式類和新式類。


舊式類


對於舊式類,類(class)和類型(type)並不完全相同。一個舊式類的實例總是繼承自一個名為instance的內置類型。如果obj是舊式類的實例,那麼obj.__class__就表示該類,但type(obj)始終是instance類型。以下示例來自Python 2.7:



新式類


新式類統一了類(class)和類型(type)的概念。如果obj是新式類的實例,type(obj)則與obj.__class__相同:




類型(Type)和類(Class)

在Python 3中,所有類都是新式類。因此,Python 3可以交換一個引用對象的類型和類。


注意:在Python 2中,默認所有類都是舊式類。在Python 2.2之前,根本不支持新式類。從Python 2.2開始,可以創建新式類,但必須明確聲明它為新式類。


請記住,在Python中,一切都是對象。類也是對象。所以一個類(class)必須有一個類型(type)。那麼類的類型是什麼呢?


考慮下面的代碼:



X的類型,正如你所想的,是類Foo,但Foo的類型,即類本身是type。一般來說,任何新式類的類型都是type。


您熟悉的內置類的類型也是type:



 就此而言,type的類型也是type(是的,確實如此):


type是一個元類,任何類都是它的實例。就像一個普通的對象是一個類的實例一樣,Python中的任何新式類以及Python 3中的任何類都是type元類的一個實例。


綜上所述:




  • x是類

    Foo

    的一個實例。



  • Foo是type元類的一個實例。



  • type也是type元類的一個實例,所以它是它自己的一個實例。



動態定義類


內置type()函數在傳遞了一個參數時將返回一個對象的類型。對於新式類,通常與對象的__class__屬性相同:


你也可以傳遞三個參數type(<name>, <bases>, <dct>)調用type():




  • <name>指定類名稱,將成為該類的__name__屬性。



  • <bases>指定繼承類的基類元組,將成為該類的__bases__屬性。



  • <dct>指定包含類主體定義的名稱空間字典,將成為該類的__dict__屬性。


以這種方式調用type()將創建一個type元類的新實例。換句話說,它動態地創建了一個新的類。


在下面每個示例中,前面的代碼片段使用type()動態地定義了一個類,後面的代碼片斷使用常用的class語句定義了類。在每種情況下,這兩個代碼片段在功能上是一樣的。


示例1


在第一個示例中,傳遞給type()的參數<bases>和<dct>都是空的,沒有指定任何父類的繼承,並且初始在命名空間字典中沒有放置任何內容。這或許是最簡單的類的定義:


示例2


這裡,<bases>是一個具有單個元素Foo的元組,指定了Bar繼承的父類。一個名為attr的屬性最初放置在命名空間字典中:




示例3


這一次,<bases>又是空的。兩個對象通過<dct>參數放置在命名空間字典中。第一個是屬性attr,第二個是函數attr_val,該函數將成為已定義類的一個方法:




示例4


上面僅用Python中的lambda定義一個非常簡單的函數。在下面的例子中,外部先定義了一個稍微複雜的函數f,然後在命名空間字典中通過函數名f分配給attr_val:



自定義元類


重新思考一下先前的這個例子:



表達式Foo()創建一個新的類Foo的實例。當解釋器遇到Foo(),將按一下順序進行解析:




  • 調用Foo父類的__call__()方法。由於Foo是標準的新式類,它的父類是type元類,所以type的__call__()方法被調用。



  • __call__()方法按以下順序進行調用:




    • __new__()



    • __init__()



    如果Foo沒有定義__new__()和__init__(),那麼將調用Foo父類的默認方法。但是如果Foo定義這些方法,就會覆蓋來自父類的方法,這就允許在實例化Foo時可以自定義行為。


    在下面的代碼中,定義了一個自定義方法new(),並將它賦值給Foo的__new__()方法:



    這會修改類Foo的實例化行為:每次Foo創建實例時,默認情況下都會將名為attr的屬性進行初始化,將該屬性設置為100。(類似於這樣的代碼通常會出現在__init__()方法中,不會出現在__new__()方法里,這個例子僅為演示目的而設計。)


    現在,正如前面重申的那樣,類也是對象。假設你想類似地在創建類Foo時自定義實例化行為。如果你要遵循上面的模式,則需要再次定義一個自定義方法,並將其指定為類Foo的實例的__new__()方法。Foo是type元類的一個實例,所以代碼如下所示:



     阿偶,你可以看到,不能重新指定元類type的__new__()方法。Python不允許這樣做。


    可以這麼講,type是派生所有新式類的元類。無論如何,你真的不應該去修改它。但是,如果你想自定義一個類的實例化,那麼有什麼辦法呢?


    一種可能的解決方案是自定義元類。本質上,不是去試圖修改type元類,而是定義自己派生於type的元類,然後對其進行修改。


    第一步是定義派生自type的元類,如下:


    頭部定義class Meta(type):指定了Meta派生自type。既然type是元類,那Meta也是一個元類。


    請注意,重新自定義了Meta的__new__()方法。因為不可能直接對type元類進行此類操作。__new__()方法執行以下操作:




    • 經由super()指代的(type)元類的__new__()方法實際創建一個新的類



    • 將自定義屬性attr分配給類,並設置值為100



    • 返回新創建的類


    現在實現代碼的另一半:定義一個新類Foo,並指定其元類為自定義元類Meta,而不是標準元類type。可以通過在類定義中使用關鍵字metaclass完成,如下所示:



     瞧! Foo已經自動擁用了從Meta元類的屬性attr。當然,你定義的任何其他類也會如此:



    就像一個類作為創建對象的模板一樣,一個元類可以作為創建類的模板。元類有時被稱為類工廠。


    比較以下兩個示例:


    對象工廠



    類工廠



    真的是必要的嗎?


    就像上面的類工廠的例子一樣簡單,它是metaclasses如何工作的本質。它們允許定製類的實例化。


    儘管如此,僅僅為了賦予每個新創建的類的自定義屬性attr,確實有點小題大做。你真的需要一個metaclass來實現嗎?


    在Python中,至少有其他一些方法可以實現同樣的效果:


    簡單的繼承



     類裝飾器



     結論


    正如蒂姆·彼得斯建議的,元類可以很容易地作為一種「尋找解決問題的方案」,通常不需要創建自定義元類。如果手頭上的問題能夠以更簡單的方式解決,那或許就應該採用。儘管如此,了解元類有助於理解Python的類,並能夠識別元類是否是工作中真正適合使用的工具。






    英文原文:https://realpython.com/python-metaclasses/


    譯者:Vincent



    喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

    本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


    請您繼續閱讀更多來自 Python程序員 的精彩文章:

    Python"ipaddress" 模塊之概述
    Pip10已正式發布

    TAG:Python程序員 |