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



我创建了一个定义__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__中它又出现了!为什么从字典中删除的关键字会被重新添加?

和传递给的字典new是传递给的对象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:nemetrismpfmpf.testtest_so4.py", line 22, in <module>
class A(metaclass=M, for_prepare='xyz'):
File "d:nemetrismpfmpf.testtest_so4.py", line 19, in __new__ 
return super().__new__(metaclass, name, bases, ns, **kwds)     
TypeError: A.__init_subclass__() takes no keyword arguments        

这不是元类的效果,而是**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是浅拷贝,添加或删除项目对另一个调用都是不可见的。

  • 不发送**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