[英]The call order of python3 metaclass
在嘗試理解metaclass
創建類實例的順序時,我感到困惑。 根據這個圖 ( 來源 ),
我鍵入以下代碼進行驗證。
class Meta(type):
def __call__(self):
print("Meta __call__")
super(Meta, self).__call__()
def __new__(mcs, name, bases, attrs, **kwargs):
print("Meta __new__")
return super().__new__(mcs, name, bases, kwargs)
def __prepare__(msc, name, **kwargs):
print("Meta __prepare__")
return {}
class SubMeta(Meta):
def __call__(self):
print("SubMeta __call__!")
super().__call__()
def __new__(mcs, name, bases, attrs, **kwargs):
print("SubMeta __new__")
return super().__new__(mcs, name, bases, kwargs)
def __prepare__(msc, name, **kwargs):
print("SubMeta __prepare__")
return Meta.__prepare__(name, kwargs)
class B(metaclass = SubMeta):
pass
b = B()
但是,結果似乎不是這樣的。
SubMeta __prepare__
Meta __prepare__
SubMeta __new__
Meta __new__
SubMeta __call__!
Meta __call__
任何幫助將不勝感激。
更新2:基於行為,下面調用M0.__call__
的事實必須是CPython源( Python/bltinmodule.c
)中builtin__build_class
中此行的 Python/bltinmodule.c
。
為了定義一個具有元類的類,我們像往常一樣調用元類的__prepare__
__new__
, __init__
__new__
和__init__
。 這創建了一個類 - 在下面的例子中, Meta
PyFunction_GET_CODE
是可調用的,但它的內部PyFunction_GET_CODE
槽不是指向它自己的 __call__
而是指向它的元類的__call__
。 因此,如果我們調用Meta()
(元類對象),我們調用M0.__call__
:
print("call Meta")
print("Meta returns:", Meta('name', (), {}))
print("finished calling Meta")
生產:
call Meta
M0 __call__: mmcls=<class '__main__.Meta'>, args=('name', (), {}), kwargs={}
Meta __new__: mcs=<class '__main__.Meta'>, name='name', bases=(), attrs={}, kwargs={}
Meta __init__: mcs=<class '__main__.name'>, name='name', bases=(), attrs={}, kwargs={}
Meta returns: <class '__main__.name'>
finished calling Meta
換句話說,我們看到Meta
行為類似於type
,但它(相當神奇而且沒有很好地記錄)調用M0.__call__
。 這無疑是由於在類的類型中查找__call__
而不是在類的實例中(實際上除了我們正在創建的實例之外沒有實例)。 這其實一般情況下:它掉出來的事實,即我們所說的__call__
上的類型 Meta
,和類型Meta
是M0
:
print("type(Meta) =", type(Meta))
打印:
type(Meta) = <class '__main__.M0'>
這解釋了它來自何處。 (我仍然認為應該在文檔中強調這一點,它也應該描述元類類型的約束 - 這些在Lib/types.py
中的_calculate_winner
中強制執行,並且作為C代碼在Objects / typeobject.c中的_PyType_CalculateMetaclass
中強制執行 。)
我不知道你的圖表來自哪里,但這是錯誤的。 更新:事實上,你的元類可以有一個元類; 看到jsbueno的回答 ,我已經更新了下面的例子。 新的句子/文本以粗體顯示,但最后一節描述了我對明顯缺乏文檔的困惑。
您現有的元類代碼至少有一個錯誤。 最重要的是,它的__prepare__
需要是一種類方法。 另請參閱使用元類的__call__方法而不是__new__? 和PEP 3115 。 並且,要使用元元類,您的元類需要擁有自己的元類, 而不是基類。
克里斯的答案包含正確的定義。 但是元類方法參數和類方法參數之間存在一些不幸的不對稱,我將在下面說明。
另一件可能__prepare__
事情是:注意在創建類B
任何實例之前調用元類__prepare__
方法:在定義class B
本身時調用它。 為了表明這一點,這里有一個更正的元類和類。 我還添加了一些插圖畫家。 我基於jsbueno的回答添加了一個元元類。 我找不到正式的Python文檔,但我已經更新了下面的輸出。
class M0(type):
def __call__(mmcls, *args, **kwargs):
print("M0 __call__: mmcls={!r}, "
"args={!r}, kwargs={!r}".format(mmcls, args, kwargs))
return super().__call__(*args, **kwargs)
class Meta(type, metaclass=M0):
def __call__(cls, *args, **kwargs):
print("Meta __call__: cls={!r}, "
"args={!r}, kwargs={!r}".format(cls, args, kwargs))
return super().__call__(*args, **kwargs)
def __new__(mcs, name, bases, attrs, **kwargs):
print("Meta __new__: mcs={!r}, name={!r}, bases={!r}, "
"attrs={!r}, kwargs={!r}".format(mcs, name, bases, attrs, kwargs))
return super().__new__(mcs, name, bases, attrs)
def __init__(mcs, name, bases, attrs, **kwargs):
print("Meta __init__: mcs={!r}, name={!r}, bases={!r}, "
"attrs={!r}, kwargs={!r}".format(mcs, name, bases, attrs, kwargs))
super().__init__(name, bases, attrs, **kwargs)
@classmethod
def __prepare__(cls, name, bases, **kwargs):
print("Meta __prepare__: name={!r}, "
"bases={!r}, kwargs={!r}".format(name, bases, kwargs))
return {}
print("about to create class A")
class A(metaclass=Meta): pass
print("finished creating class A")
print("about to create class B")
class B(A, metaclass=Meta, foo=3):
@staticmethod
def __new__(cls, *args, **kwargs):
print("B __new__: cls={!r}, "
"args={!r}, kwargs={!r}".format(cls, args, kwargs))
return super().__new__(cls)
def __init__(self, *args, **kwargs):
print("B __init__: args={!r}, kwargs={!r}, ".format(args, kwargs))
print("finished creating class B")
print("about to create instance b = B()")
b = B('hello', bar=7)
print("finished creating instance b")
現在,讓我們觀察一下當我運行它時會發生什么,並將每一塊分開:
$ python3.6 meta.py
about to create class A
Meta __prepare__: name='A', bases=(), kwargs={}
M0 __call__: mmcls=<class '__main__.Meta'>, args=('A', (), {'__module__': '__main__', '__qualname__': 'A'}), kwargs={}
Meta __new__: mcs=<class '__main__.Meta'>, name='A', bases=(), attrs={'__module__': '__main__', '__qualname__': 'A'}, kwargs={}
Meta __init__: mcs=<class '__main__.A'>, name='A', bases=(), attrs={'__module__': '__main__', '__qualname__': 'A'}, kwargs={}
finished creating class A
為了創建類A
本身,Python首先調用元類的__prepare__
,為它提供類的名稱( A
),基類列表(一個空元組 - 它被稱為列表,但實際上是一個元組),以及任何關鍵字參數(沒有)。 正如PEP 3115所指出的,元類需要返回一個字典或類似dict
的對象; 這個只是返回一個空字典,所以我們在這里很好。
(我不會在這里打印cls
,但是如果你這樣做,你會發現它只是<class '__main__.Meta'>
。)
接下來,從__prepare__
獲得字典后,Python 首先調用元元__call__
,即M0.__call__
,將整個參數集作為args
元組傳遞。 然后它使用該類的所有屬性填充__prepare__
-supplied字典,將其作為attrs
傳遞給元類__new__
和__init__
。 如果您打印從__prepare__
返回的字典的id
並傳遞給__new__
和__init__
您將看到它們都匹配。
由於A
類沒有方法或數據成員,因此我們在此處僅看到了magic __module__
和__qualname__
屬性。 我們也看不到關鍵字參數,所以現在讓我們繼續創建B
類:
about to create class B
Meta __prepare__: name='B', bases=(<class '__main__.A'>,), kwargs={'foo': 3}
M0 __call__: mmcls=<class '__main__.Meta'>, args=('B', (<class '__main__.A'>,), {'__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0a58>, '__init__': <function B.__init__ at 0x800ad2840>, '__classcell__': <cell at 0x800a749d8: empty>}), kwargs={'foo': 3}
Meta __new__: mcs=<class '__main__.Meta'>, name='B', bases=(<class '__main__.A'>,), attrs={'__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0940>, '__init__': <function B.__init__ at 0x800ad27b8>, '__classcell__': <cell at 0x800a745b8: empty>}, kwargs={'foo': 3}
Meta __init__: mcs=<class '__main__.B'>, name='B', bases=(<class '__main__.A'>,), attrs={'__module__': '__main__', '__qualname__': 'B', '__new__': <staticmethod object at 0x800ad0940>, '__init__': <function B.__init__ at 0x800ad27b8>, '__classcell__': <cell at 0x800a745b8: Meta object at 0x802047018>}, kwargs={'foo': 3}
finished creating class B
這個更有趣。 現在我們有一個基類,即__main__.A
。 B
類還定義了幾種方法( __new__
和__init__
),我們在傳遞給元類__new__
和__init__
方法的attrs
字典中看到它們(記住,它們只是元類的__prepare__
返回的現在填充的字典)。 和以前一樣,傳遞通過元元類M0.__call__
。 我們還看到一個關鍵字參數, {'foo': 3}
。 在屬性字典中,我們還可以觀察magic __classcell__
條目:請參閱為Python 3.6元類提供__classcell__示例,以獲得關於這是什么的簡短描述,但是,呃, 超級 -短,它是用於使super()
工作。
關鍵字參數傳遞給所有三個元類方法,以及元元類的方法。 (我不太清楚為什么。請注意,修改任何元類方法中的字典不會影響其他任何方法,因為它是每次原始關鍵字參數的副本。 但是,我們可以在元元數據中修改它class:將kwargs.pop('foo', None)
到M0.__call__
以觀察此情況。 )
現在我們有了A
和B
類,我們可以繼續創建B
類的實際實例。 現在我們看到調用元類的__call__
(不是元元類):
about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs={'bar': 7}
可以改變傳遞的args
或kwargs
,但我沒有; 上面的示例代碼結束了調用type.__call__(cls, *args, **kwargs)
(通過super().__call__
)。 這反過來調用B.__new__
和B.__init__
:
B __new__: cls=<class '__main__.B'>, args=('hello',), kwargs={'bar': 7}
B __init__: args=('hello',), kwargs={'bar': 7},
finished creating instance b
它完成了B
類新實例的實現,然后我們將其綁定到名稱b
。
注意B.__new__
說:
return super().__new__(cls)
所以我們調用object.__new__
來創建實例 - 這或多或少是所有Python版本的要求; 當你返回一個單例實例時(理想情況下,一個不可修改的實例),你只能“作弊”。 它是type.__call__
,在這個對象上調用B.__init__
,傳遞我們傳遞它的參數和關鍵字參數。 如果我們將Meta
的__call__
替換為:
def __call__(cls, *args, **kwargs):
print("Meta __call__: cls={!r}, "
"args={!r}, kwargs={!r}".format(cls, args, kwargs))
return object.__new__(cls)
我們將看到B.__new__
和B.__init__
從未被調用:
about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs={'bar': 7}
finished creating instance b
實際上,這將創建一個無用/未初始化的實例b
。 因此,元類__call__
方法調用底層類的__init__
是很關鍵的,通常是通過super().__call__
調用type.__call__
super().__call__
。 如果底層類具有__new__
,則元類應首先調用type.__call__
,通常再次調用type.__call__
。
引用第3.3.3.6節:
通過執行類主體填充類命名空間后,通過調用
metaclass(name, bases, namespace, **kwds)
創建類對象(此處傳遞的其他關鍵字與傳遞給__prepare__
關鍵字相同)。
這解釋了在創建b
作為B
類實例時對Meta.__call__
的調用,而不是在創建類A
和B
本身時調用Meta.__new__
和Meta.__init__
之前Python首先調用M0.__call__
的事實。
下一段提到了__classcell__
條目; 之后繼續描述__set_name__
和__init_subclass__
鈎子的使用。 這里沒有任何內容告訴我們此時Python如何或為何調用M0.__call__
。
之前,在3.3.3.3到3.3.3.5節中,文檔描述了確定元類,准備類命名空間和執行類主體的過程。 這是應該描述元元類操作的地方,但事實並非如此。
其他幾個部分描述了一些額外的約束。 一個重要的是3.3.10,它討論了如何通過對象類型找到特殊方法,繞過常規成員屬性查找甚至(有時)一個元類getattribute,說:
以這種方式繞過
__getattribute__()
機制為解釋器中的速度優化提供了很大的空間,代價是在處理特殊方法時有一定的靈活性( 必須在類對象本身上設置特殊方法才能一致地調用翻譯)。
更新2:這實際上是技巧的秘訣:通過類型的類型找到特殊的__call__
方法。 如果元類具有元類,則元元類提供__call__
槽; 否則,元類的type
是type
,因此__call__
槽是type.__call__
。
盡管@ torek的長篇答案,以及關於課堂創作的許多其他細節,你在這個問題上匯集的內容大多是正確的。
你的代碼中唯一錯誤的就是讓你感到困惑的是你調用Meta
te類本身就是SubMeta
的元類 ,而不是它的父SubMeta
。
只需將Submeta
聲明更改為:
class SubMeta(type, metaclass=Meta):
...
(也不需要它繼承“Meta” - 它只能從type
派生。除此之外,我想要定制type.__call__
,這對於創建類的實例同時是有用的(當SubMeta.__call__
時,你的類本身(調用Meta.__call__
))
這是我剛在終端輸入的另一個較短的例子。 很抱歉命名不一致,並且不太完整 - 但它顯示了要點:
class M(type):
def __call__(mmcls, *args, **kwargs):
print("M's call", args, kwargs)
return super().__call__(*args, **kwargs)
class MM(type, metaclass=M):
def __prepare__(cls, *args, **kw):
print("MM Prepare")
return {}
def __new__(mcls, *args, **kw):
print("MM __new__")
return super().__new__(mcls, *args, **kw)
class klass(metaclass=MM):
pass
在處理klass
體時,Python輸出是:
MM Prepare
M's call ('klass', (), {'__module__': '__main__', '__qualname__': 'klass'}) {}
MM __new__
正如您所看到的,使用元元類,可以自定義元類__init__
和__new__
的調用順序和參數, 但仍然存在無法從純Python代碼自定義的步驟,並且需要對API的本機調用(以及可能的原始對象結構操作) - 即:
__prepare__
的調用 __init_subclass__
的調用 __set_name__
最后兩個項目發生在meta-meta的__call__
返回之后,並且在將流程恢復到類模塊所在的模塊之前。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.