Python 的元類與元編程

Python元類和元編程

術語元編程是指程序具備瞭解或操縱自身的潛力。Python支持一種稱爲metaclasses的類的元編程形式。html

元類是一個深奧的OOP概念,幾乎隱藏在全部Python代碼以後。不管您是否知道,都在使用它們。在大多數狀況下,您無需意識到這一點。大多數Python程序員不多(即便有的話)也沒必要考慮元類。python

可是,當須要時,Python提供了並不是全部面向對象的語言都支持的功能:您能夠深刻了解並自定義元類。自定義元類的使用引發了一些爭議,正如Python 禪意做者Tim Peters所引用的那樣程序員

元類具備比99%的用戶應該擔憂的更深的魔力。若是您想知道是否須要它們,則不須要(實際上須要它們的人確定會知道他們須要它們,而且不須要解釋緣由)。」

蒂姆·彼得斯shell

有一些使用者(Pythonista - 衆所周知的Python愛好者)認爲永遠不要使用自定義元類。這可能有點遠,可是極可能不須要自定義元類。若是不是很明顯有問題須要解決,那麼若是以更簡單的方式解決問題,它可能會更乾淨,更易讀。編程

儘管如此,理解 Python 元類仍是值得的,由於經過元類能夠更好地理解Python 類的內部。可能有一天會遇到一種狀況:只須要一個自定義元類便可以解決問題。函數

舊式與新式類

在Python領域中,類能夠是兩個變體之一。還沒有肯定官方術語,所以將它們非正式地稱爲舊類和新類。ui

老式類

對於老式的類,類和類型不是一回事。老式類的實例始終由稱爲的單個內置類型實現instance。若是obj是老式類的實例,則obj.__class__指定該類,但type(obj)始終爲instance。如下示例取自 Python 2.7:this

>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x.__class__
<class __main__.Foo at 0x000000000535CC48>
>>> type(x)
<type 'instance'>

新型類

新型類統一了類和類型的概念。若是obj是新型類的實例,type(obj)則與相同obj.__class__spa

>>> class Foo:
...     pass
>>> obj = Foo()
>>> obj.__class__
<class '__main__.Foo'>
>>> type(obj)
<class '__main__.Foo'>
>>> obj.__class__ is type(obj)
True
>>> n = 5
>>> d = { 'x' : 1, 'y' : 2 }

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> for obj in (n, d, x):
...     print(type(obj) is obj.__class__)
...
True
True
True

類型type和類別class

在Python 3中,全部類都是新型類。所以,在Python 3中,能夠互換地引用對象的類型及其類是合理的。設計

注意:在Python 2中,默認狀況下,類爲舊樣式。在Python 2.2以前,根本不支持新型類。從Python 2.2開始,能夠建立它們,但必須將其顯式聲明爲new-style。

請記住,在Python中,一切都是對象。類也是對象。結果,一個類必須具備一個類型。什麼是課程類型?

考慮如下:

>>> class Foo:
...     pass
...
>>> x = Foo()

>>> type(x)
<class '__main__.Foo'>

>>> type(Foo)
<class 'type'>

如您所料,type x是class Foo。可是Foo,類自己的類型是type。一般,任何新式類的類型都是type

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

>>> for t in int, float, dict, list, tuple:
...     print(type(t))
...
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>
<class 'type'>

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

>>> type(type)
<class 'type'>

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

在上述狀況下:

  • x是class的實例Foo
  • Footype元類的實例。
  • type也是type元類的實例,所以它也是自身的實例。

Python類鏈

動態定義類

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

>>> type(3)
<class 'int'>
>>> type(['foo', 'bar', 'baz'])
<class 'list'>
>>> t = (1, 2, 3, 4, 5)
>>> type(t)
<class 'tuple'>
>>> class Foo:
...     pass
...
>>> type(Foo())
<class '__main__.Foo'>

還可使用三個參數進行調用type(<name>, <bases>, <dct>)

  • <name>指定類名稱。這成爲__name__該類的屬性。
  • <bases>指定從其繼承的基類的元組。這成爲__bases__該類的屬性。
  • <dct>指定一個包含類主體定義的名稱空間字典。這成爲__dict__該類的屬性。

type()以這種方式進行調用會建立該type元類的新實例。換句話說,它動態建立一個新類

在如下每一個示例中,最上面的代碼段使用來動態定義一個類type(),而下面的代碼段則使用該class語句以一般的方式定義該類。在每種狀況下,這兩個代碼段在功能上是等效的。

例子1

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

>>> Foo = type('Foo', (), {})

>>> x = Foo()
>>> x
<__main__.Foo object at 0x04CFAD50>
>>> class Foo:
...     pass
...
>>> x = Foo()
>>> x
<__main__.Foo object at 0x0370AD50>

例子2

這裏<bases>是一個具備單個元素的元組Foo,指定Bar從其繼承的父類。屬性attr最初放置在名稱空間字典中:

>>> Bar = type('Bar', (Foo,), dict(attr=100))

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)
>>> class Bar(Foo):
...     attr = 100
...

>>> x = Bar()
>>> x.attr
100
>>> x.__class__
<class '__main__.Bar'>
>>> x.__class__.__bases__
(<class '__main__.Foo'>,)

例子3

此次,又<bases>是空的。經過<dct>參數將兩個對象放入名稱空間字典中。第一個是名爲的屬性attr,第二個是名爲的函數attr_val,該函數成爲已定義類的方法:

>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': lambda x : x.attr
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100
>>> class Foo:
...     attr = 100
...     def attr_val(self):
...         return self.attr
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
100

例子4

lambda在Python中只能定義很是簡單的函數。在下面的示例中,在外部定義了一個稍微複雜一點的函數,而後attr_val經過名稱在名稱空間字典中將其分配給f

>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> Foo = type(
...     'Foo',
...     (),
...     {
...         'attr': 100,
...         'attr_val': f
...     }
... )

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100
>>> def f(obj):
...     print('attr =', obj.attr)
...
>>> class Foo:
...     attr = 100
...     attr_val = f
...

>>> x = Foo()
>>> x.attr
100
>>> x.attr_val()
attr = 100

自定義元類

再次考慮這個陳舊的示例:

>>> class Foo:
...     pass
...
>>> f = Foo()

該表達式Foo()建立class的新實例Foo。解釋器遇到時Foo(),將發生如下狀況:

  • 的父類的__call__()方法Foo被調用。因爲Foo是標準的新型類,所以其父類是type元類,所以調用type__call__()方法。
  • __call__()方法依次調用如下內容:

    • __new__()
    • __init__()

若是Foo未定義__new__()__init__(),則默認方法繼承自Foo的祖先。可是,若是Foo確實定義了這些方法,則它們會覆蓋祖先中的方法,從而在實例化時容許自定義行爲Foo

在下面,定義了一個自定義方法,並將new()其指定爲__new__()用於的方法Foo

>>> def new(cls):
...     x = object.__new__(cls)
...     x.attr = 100
...     return x
...
>>> Foo.__new__ = new

>>> f = Foo()
>>> f.attr
100

>>> g = Foo()
>>> g.attr
100

這會修改類的實例化行爲Foo:每次Foo建立實例時,默認狀況下都會使用名爲的屬性對其進行初始化,該屬性attr的值爲100。(這樣的代碼一般會出如今__init__()方法中,而一般不會出如今方法中__new__(),這個示例是爲演示目的而設計的)

如今,正如已經重申的,類也是對象。假設您要在建立相似的類時,能夠以相似的自定義方式完成Foo實例化行爲。若是要遵循上述模式,須要再次定義一個自定義方法,並將其分配__new__()爲該類Foo是實例的方法。Footype元類的實例,所以代碼以下所示:

# Spoiler alert:  This doesn't work!
>>> def new(cls):
...     x = type.__new__(cls)
...     x.attr = 100
...     return x
...
>>> type.__new__ = new
Traceback (most recent call last):
  File "<pyshell#77>", line 1, in <module>
    type.__new__ = new
TypeError: can't set attributes of built-in/extension type 'type'

如您所見,除了不能從新分配元類type__new__()方法。Python不容許這樣作。

這可能也是同樣。type是從其派生全部新樣式類的元類。不管如何,您真的不該該對此亂搞。可是,若是要自定義類的實例化,那又有什麼辦法?

一種可能的解決方案是自定義元類。本質上,您沒必要定義type元類,而能夠定義本身的元類,該元類是從派生的type,而後您就可使用元類。

第一步是定義一個從派生的元類,type以下所示:

>>> class Meta(type):
...     def __new__(cls, name, bases, dct):
...         x = super().__new__(cls, name, bases, dct)
...         x.attr = 100
...         return x
...

定義class Meta(type):聲明,指定Metatype派生。因爲type是一個元類,所以也構成Meta了一個元類。

請注意,__new__()已爲定義了自定義方法Meta。沒法type直接對元類執行此操做。該__new__()方法執行如下操做:

  • 由父元類(即type)的代理--super()__new__()方法建立一個新的類
  • 將自定義屬性分配attr給類,其值爲100
  • 返回新建立的類

如今,巫毒教的另外一半:定義一個新類Foo,並指定其元類是自定義元類Meta,而不是標準元類type。使用metaclass類定義中的關鍵字來完成此操做,以下所示:

>>> class Foo(metaclass=Meta):
...     pass
...
>>> Foo.attr
100

瞧! Foo已經拿到Meta元類的attr自動屬性。固然,相似定義的任何其餘類也將這樣作:

>>> class Bar(metaclass=Meta):
...     pass
...
>>> class Qux(metaclass=Meta):
...     pass
...
>>> Bar.attr, Qux.attr
(100, 100)

與類充當建立對象的模板的方式相同,元類充當建立類的模板。元類有時稱爲類工廠)。

比較如下兩個示例:

對象工廠:

>>> class Foo:
...     def __init__(self):
...         self.attr = 100
...

>>> x = Foo()
>>> x.attr
100

>>> y = Foo()
>>> y.attr
100

>>> z = Foo()
>>> z.attr
100

類工廠:

>>> class Meta(type):
...     def __init__(
...         cls, name, bases, dct
...     ):
...         cls.attr = 100
...
>>> class X(metaclass=Meta):
...     pass
...
>>> X.attr
100

>>> class Y(metaclass=Meta):
...     pass
...
>>> Y.attr
100

>>> class Z(metaclass=Meta):
...     pass
...
>>> Z.attr
100

這真的有必要嗎?

就像上面的類工廠示例同樣簡單,這是元類如何工做的本質。它們容許自定義類如何實例化。

儘管如此,attr在每一個新建立的類上賦予自定義屬性仍然有不少麻煩。您真的須要一個元類嗎?

在 Python 中,至少有幾種其餘方法能夠有效地完成同一件事:

簡單繼承:

>>> class Base:
...     attr = 100
...

>>> class X(Base):
...     pass
...

>>> class Y(Base):
...     pass
...

>>> class Z(Base):
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

類裝飾器:

>>> def decorator(cls):
...     class NewClass(cls):
...         attr = 100
...     return NewClass
...
>>> @decorator
... class X:
...     pass
...
>>> @decorator
... class Y:
...     pass
...
>>> @decorator
... class Z:
...     pass
...

>>> X.attr
100
>>> Y.attr
100
>>> Z.attr
100

結論

正如蒂姆·彼得斯(Tim Peters)所建議的那樣,元類很容易進入「從問題中尋找解決方案」的境界。一般不須要建立自定義元類。若是眼前的問題能夠用更簡單的方法解決,那就應該這樣解決。儘管如此,理解元類仍是有好處的,這樣您就能夠大體理解Python類,並能夠識別什麼時候才真正適合使用元類。

🐍Python技巧💌

Python技巧字典合併

關於做者:
約翰·斯圖茲
奧爾登·桑托斯
丹·巴德
喬安娜·賈布隆斯基


❤️快樂Pythoning!

彩蛋

>>> import this
相關文章
相關標籤/搜索