为什么 class 定义的关键字参数在被删除后会重新出现?

Why do keyword arguments to a class definition reappear after they were removed?

我创建了一个定义 __prepare__ 方法的元 class,它应该使用 class 定义中的特定关键字,如下所示:

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

当我 运行 它时,class 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__ 中又出现了!为什么从字典中删除的关键字会重新添加?

and the dict that is passed to new is the same object that was passed to prepare

不幸的是,这就是你错的地方。

Python只回收相同的对象id。

如果你在 __prepare__ 中创建一个新的字典,你会注意到 kwds 的 id 在 __new__ 中发生了变化。

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        
  • 不要将**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

这不是元类的影响,而是 **kwargs 的影响。每当使用 **kwargs 调用一个函数时,当前字典将被解包 并且不会传递给 。每当一个函数接收到 **kwargs一个新的字典被创建

实际上,当 caller/callee 都使用 **kwargs 时,其中任何一个看到的字典都是 copy.

比较单独使用 **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

同样,__prepare____new__在创建class时分别调用。他们的 **kwargs 是浅拷贝,添加或删除项目对另一个调用是不可见的。