为啥类定义的关键字参数在删除后会重新出现?
Posted
技术标签:
【中文标题】为啥类定义的关键字参数在删除后会重新出现?【英文标题】:Why do keyword arguments to a class definition reappear after they were removed?为什么类定义的关键字参数在删除后会重新出现? 【发布时间】:2021-11-28 06:14:09 【问题描述】:我创建了一个定义__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
项目已从 dict 中删除,传递给 __new__
的 dict 与传递给 __prepare__
的对象相同,并且与 for_prepare
项目所在的对象相同突然出现,但在__new__
它又出现了!为什么从字典中删除的关键字会重新添加?
【问题讨论】:
你明白了。正在发生的事情的答案,但似乎没有人涉及避免您遇到的错误的正确方法的主题:__init_subclass__
应该始终以协作方式编写,接收 **kwargs,并使用任何参数调用 supper不消耗。不幸的是,__init_subclass__
显然必须设置为任何具有采用 KW 参数的元类的类并使用它们。
【参考方案1】:
传递给 new 的 dict 与传递给 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
【讨论】:
因此,如果 Python 将带有类定义关键字参数的新 dict 传递给__new__
,如果我想将类定义关键字传递给 __prepare__
,我必须定义 __new__
,它什么都不做但忽略该关键字?
好吧,正如@MisterMiyagi 指出的那样,您并没有真正传递字典。您正在通过被调用的函数将您的 dict 的压缩版本“打包回”到一个新的 dict 中。在您的情况下,“旧”字典恰好从内存中删除,然后使用相同的 ID 创建新字典。所以是的,如果你想删除__new__
中的字典项,你应该在__new__
中删除它。或者,也许您可以更改您的课程以实际传递 dict 而不是 **kwargs
【参考方案2】:
这不是元类的影响,而是**kwargs
的影响。每当使用 **kwargs
调用函数时,当前的 dict 都会被解包而不是传递。每当函数接收到**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
同样,__prepare__
和 __new__
在创建 class
时会分别调用。他们的**kwargs
是浅拷贝,其他调用都看不到添加或删除项目。
【讨论】:
【参考方案3】: 不要将**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
【讨论】:
以上是关于为啥类定义的关键字参数在删除后会重新出现?的主要内容,如果未能解决你的问题,请参考以下文章
为啥此 Django API 调用出现意外的关键字参数错误?
为啥某些 Flask 会话值在关闭浏览器窗口后会从会话中消失,但稍后会在没有我添加它们的情况下重新出现?
由于将在索引 主关键字或关系中创建重复的值,请求对表的改变没有成功。 改变该字段中的或包含重复数据的字段中的数据,删除索引或重新定义索引以允许重复的值并再试一次。
由于将在索引 主关键字或关系中创建重复的值,请求对表的改变没有成功。 改变该字段中的或包含重复数据的字段中的数据,删除索引或重新定义索引以允许重复的值并再试一次。