簡體   English   中英

python3元類的調用順序

[英]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 ,和類型MetaM0

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__以觀察此情況。

現在我們有了AB類,我們可以繼續創建B類的實際實例。 現在我們看到調用元類的__call__ (不是元元類):

about to create instance b = B()
Meta __call__: cls=<class '__main__.B'>, args=('hello',), kwargs={'bar': 7}

可以改變傳遞的argskwargs ,但我沒有; 上面的示例代碼結束了調用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__的調用,而不是在創建類AB本身時調用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__槽; 否則,元類的typetype ,因此__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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM