來源: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(
-
指定類名稱,將成為該類的__name__屬性。 -
指定繼承類的基類元組,將成為該類的__bases__屬性。 -
指定包含類主體定義的名稱空間字典,將成為該類的__dict__屬性。
以這種方式呼叫type()將建立一個type元類的新實體。換句話說,它動態地建立了一個新的類。
在下麵每個示例中,前面的程式碼片段使用type()動態地定義了一個類,後面的程式碼片斷使用常用的class陳述句定義了類。在每種情況下,這兩個程式碼片段在功能上是一樣的。
示例1
在第一個示例中,傳遞給type()的引數
示例2
這裡,
示例3
這一次,
示例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好文請點選【閱讀原文】哦
↓↓↓