歡迎光臨
每天分享高質量文章

Python黑魔法:元類

來源:Python程式員

ID:pythonbuluo

術語“超程式設計”指的是程式具有編寫或操縱其自身作為它們資料的潛力。Python支援稱為元類的類的超程式設計。

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

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

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

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

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

                                                      

舊式類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(, , )呼叫type():

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

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

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

以這種方式呼叫type()將建立一個type元類的新實體。換句話說,它動態地建立了一個新的類。

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

示例1

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

示例2

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

示例3

這一次,又是空的。兩個物件透過引數放置在名稱空間字典中。第一個是屬性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人工智慧和全棧開發》2018年07月23日即將在北京開課,120天衝擊Python年薪30萬,改變速約~~~~

    *宣告:推送內容及圖片來源於網路,部分內容會有所改動,版權歸原作者所有,如來源資訊有誤或侵犯權益,請聯絡我們刪除或授權事宜。

    – END –


    更多Python好文請點選【閱讀原文】哦

    ↓↓↓

贊(0)

分享創造快樂