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"ipaddress" 模塊之概述
※Pip10已正式發布
TAG:Python程序員 |