类定义中的关键字参数被移除后为什么会再次出现?

6

我创建了一个元类,定义了__prepare__方法,该方法应该在类定义中消耗特定的关键字,如下所示:

class M(type):
    @classmethod
    def __prepare__(metaclass, name, bases, **kwds):
        print('in M.__prepare__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {kwds=}\n  {id(kwds)=}')
        if 'for_prepare' not in kwds:
            return super().__prepare__(name, bases, **kwds)
        arg = kwds.pop('for_prepare')
        print(f'  arg popped for prepare: {arg}')
        print(f'  end of prepare: {kwds=} {id(kwds)=}')
        return super().__prepare__(name, bases, **kwds)

    def __new__(metaclass, name, bases, ns, **kwds):
        print('in M.__new__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {ns=}\n  {kwds=}\n  {id(kwds)=}')
        return super().__new__(metaclass, name, bases, ns, **kwds)


class A(metaclass=M, for_prepare='xyz'):
    pass

当我运行它时,在 A 类的定义中,for_prepare 关键字参数在 __new__ 中再次出现(稍后在 __init_subclass__ 中也有出现,导致出错):

$ python3 ./weird_prepare.py
in M.__prepare__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  kwds={'for_prepare': 'xyz'}
  id(kwds)=140128409916224
  arg popped for prepare: xyz
  end of prepare: kwds={} id(kwds)=140128409916224
in M.__new__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  ns={'__module__': '__main__', '__qualname__': 'A'}
  kwds={'for_prepare': 'xyz'}
  id(kwds)=140128409916224
Traceback (most recent call last):
  File "./weird_prepare.py", line 21, in <module>
    class A(metaclass=M, for_prepare='xyz'):
  File "./weird_prepare.py", line 18, in __new__
    return super().__new__(metaclass, name, bases, ns, **kwds)
TypeError: __init_subclass__() takes no keyword arguments

如您所见,字典中的for_prepare项目已被移除,传递给__new__的字典与传递给__prepare__的相同,并且从其中弹出了for_prepare项目,但在__new__中它又出现了!为什么已经从字典中删除的关键词会重新添加回去呢?

你已经得到了关于正在发生的事情的答案,但是似乎没有任何一个触及到避免你所遇到的错误的正确方法:__init_subclass__ 应该总是以协作的方式编写,接收 **kwargs,并使用未处理的任何参数调用 super。不幸的是,__init_subclass__ 显然必须设置为具有接受 KW 参数并消耗它们的元类的任何类。 - jsbueno
3个回答

6

传递给 new 的字典与传递给 prepare 的对象是同一个。

不幸的是,你的想法是错误的。

Python 只会回收相同的对象ID。

如果在 __prepare__ 中创建一个新的字典,你会注意到在 __new__kwds 的ID发生了改变。

class M(type):
    @classmethod
    def __prepare__(metaclass, name, bases, **kwds):
        print('in M.__prepare__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {kwds=}\n  {id(kwds)=}')
        if 'for_prepare' not in kwds:
            return super().__prepare__(name, bases, **kwds)
        arg = kwds.pop('for_prepare')
        x = {} # <<< create a new dict
        print(f'  arg popped for prepare: {arg}')
        print(f'  end of prepare: {kwds=} {id(kwds)=}')
        return super().__prepare__(name, bases, **kwds)

    def __new__(metaclass, name, bases, ns, **kwds):
        print('in M.__new__:')
        print(f'  {metaclass=}\n  {name=}\n'
              f'  {bases=}\n  {ns=}\n  {kwds=}\n  {id(kwds)=}')
        return super().__new__(metaclass, name, bases, ns, **kwds)


class A(metaclass=M, for_prepare='xyz'):
    pass

输出:

in M.__prepare__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  kwds={'for_prepare': 'xyz'}
  id(kwds)=2595838763072
  arg popped for prepare: xyz
  end of prepare: kwds={} id(kwds)=2595838763072
in M.__new__:
  metaclass=<class '__main__.M'>
  name='A'
  bases=()
  ns={'__module__': '__main__', '__qualname__': 'A'}
  kwds={'for_prepare': 'xyz'}
  id(kwds)=2595836298496 # <<< id has changed now
Traceback (most recent call last):
  File "d:\nemetris\mpf\mpf.test\test_so4.py", line 22, in <module>
    class A(metaclass=M, for_prepare='xyz'):
  File "d:\nemetris\mpf\mpf.test\test_so4.py", line 19, in __new__ 
    return super().__new__(metaclass, name, bases, ns, **kwds)     
TypeError: A.__init_subclass__() takes no keyword arguments        

所以如果Python通过类定义关键字参数将新字典传递给__new__,如果我想将类定义关键字传递给__prepare__,我必须定义一个什么都不做但忽略该关键字的__new__吗? - sagittarian
好吧,正如@MisterMiyagi指出的那样,你并没有真正传递一个字典。你正在传递一个解包版本的字典,由被调用的函数将其“重新打包”成一个新的字典。在你的情况下,“旧”的字典恰好从内存中删除,然后使用相同的id创建新的字典。所以是的,如果你想在__new__中摆脱一个字典项,你应该在__new__中将其删除。或者也许你可以改变你的类,实际上传递一个字典而不是**kwargs - Mike Scotty

4
这不是元类的影响,而是**kwargs的影响。每当使用**kwargs调用函数时,当前字典都会被解包,而不是传递下去。每当函数接收到**kwargs时,都会创建一个新的字典。
实际上,在调用者和被调用者都使用**kwargs的情况下,两者看到的字典都是副本。
比较只使用**kwargs的设置:
def first(**kwargs):
    print(f"Popped 'some_arg': {kwargs.pop('some_arg')!r}")

def second(**kwargs):
    print(f"Got {kwargs} in the end")

def head(**kwargs):
    first(**kwargs)
    second(**kwargs)

head(a=2, b=3, some_arg="Watch this!", c=4)
# Popped 'some_arg': 'Watch this!'
# Got {'a': 2, 'b': 3, 'some_arg': 'Watch this!', 'c': 4} in the end

同样地,创建一个class时,分别调用__prepare____new__。它们的**kwargs是浅复制,添加或删除项对另一个调用不可见。

1
  • 不要将**kwds发送到__new__,在Python 3.6之后它将无法捕获。

例子

class M(type):
    @classmethod
    def __prepare__(metaclass, name, bases, **kwds):
        # print('in M.__prepare__:')
        # print(f'  {metaclass}=\n  {name}=\n'
        #       f'  {bases}=\n  {kwds}=\n  {id(kwds)}=')
        if 'for_prepare' not in kwds:
            return super().__prepare__(name, bases, **kwds)
        # arg = kwds.pop('for_prepare')
        # print(f'  arg popped for prepare: {arg}')
        # print(f'  end of prepare: {kwds}= {id(kwds)}=')
        return super().__prepare__(name, bases, **kwds)

    def __new__(metaclass, name, bases, ns, **kwds):
        print('in M.__new__:')
        print(f'  metaclass = {metaclass}\n  name = {name}\n'
              f'  bases = {bases}\n  ns = {ns}\n  kwds = {kwds}\n  id_kwds = {id(kwds)}')
        return super().__new__(metaclass, name, bases, ns)


class A(metaclass=M, for_prepare='xyz'):
    pass

a = A()

结果:

in M.__new__:
  metaclass = <class '__main__.M'>
  name = A
  bases = ()
  ns = {'__module__': '__main__', '__qualname__': 'A'}
  kwds = {'for_prepare': 'xyz'}
  id_kwds = 2101285477256

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接