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

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

【讨论】:

以上是关于为啥类定义的关键字参数在删除后会重新出现?的主要内容,如果未能解决你的问题,请参考以下文章

苹果手机出现logintype为必要参数

为啥此 Django API 调用出现意外的关键字参数错误?

C#中为啥使用字段封装

为啥某些 Flask 会话值在关闭浏览器窗口后会从会话中消失,但稍后会在没有我添加它们的情况下重新出现?

由于将在索引 主关键字或关系中创建重复的值,请求对表的改变没有成功。 改变该字段中的或包含重复数据的字段中的数据,删除索引或重新定义索引以允许重复的值并再试一次。

由于将在索引 主关键字或关系中创建重复的值,请求对表的改变没有成功。 改变该字段中的或包含重复数据的字段中的数据,删除索引或重新定义索引以允许重复的值并再试一次。