【问题标题】:Python patching __new__ methodPython 修补 __new__ 方法
【发布时间】:2020-12-18 16:37:48
【问题描述】:

我正在尝试修补类的 __new__ 方法,但它没有按预期工作。

from contextlib import contextmanager

class A:
    def __init__(self, arg):
        print('A init', arg)

@contextmanager
def patch_a():
    new = A.__new__

    def fake_new(cls, *args, **kwargs):
        print('call fake_new')
        return new(cls, *args, **kwargs) 
        # here I get error: TypeError: object.__new__() takes exactly one argument (the type to instantiate)

    A.__new__ = fake_new
    try:
        yield
    finally:
        A.__new__ = new

if __name__ == '__main__':
    A('foo')
    with patch_a():
        A('bar')
    A('baz')

我希望得到以下输出:

A init foo
call fake_new
A init bar
A init baz

但在call fake_new 之后出现错误(请参阅代码中的注释)。 对我来说似乎我只是装饰了一个__new__ 方法并传播所有参数不变。 它不起作用,原因对我来说是模糊的。

我也可以写return new(cls) 并调用A('bar') 工作正常。但随后A('baz') 中断。

有人能解释一下是怎么回事吗?

Python 版本为 3.8

【问题讨论】:

  • A.__new__ is object.__new__ 只接受一个参数。你应该把它简单地称为new(cls)。我不明白为什么与'baz' 的通话失败......奇怪......

标签: python python-3.x class


【解决方案1】:

您遇到了 Python 对象实例化的一个复杂部分 - 在该部分中,该语言选择了一种设计,该设计允许人们创建带有参数的自定义 __init__ 方法,而无需接触 __new__

但是,在类层次结构的基础object 中,__new____init__ 各有一个参数。

IIRC,它是这样的:如果你的类有一个自定义的 __init__ 而你没有触及 __new__ 并且类实例化还有更多的参数将传递给 __init____new__ ,参数将从调用 do __new__ 中剥离,因此您不必为了吞下您在 __init__ 中使用的参数而对其进行自定义。反之亦然:如果您的类有一个带有额外参数的自定义__new__,并且没有自定义__init__,则这些不会传递给object.__init__

在您的设计中,Python 会看到一个自定义 __new__ 并将传递给 __init__ 的相同额外参数传递给它 - 通过使用 *args, **kw,您将这些参数转发给接受单个参数的 object.__new__ - 和你得到了你向我们展示的错误。

解决方法是不要将这些额外参数传递给原始的 __new__ 方法 - 除非那里需要它们 - 所以你必须在启动对象时对 Python 的类型进行相同的检查。

还有一个有趣的惊喜:在使示例运行时,我发现 即使A.__new__ 恢复补丁时被删除,仍然被cPython的type实例化认为是“触动”了,传参。 为了让您的代码正常工作,我需要留下一个永久存根 A.__new__,它只会转发 cls 参数:


from contextlib import contextmanager

class A:
    def __init__(self, arg):
        print('A init', arg)

@contextmanager
def patch_a():
    new = A.__new__

    def fake_new(cls, *args, **kwargs):
        print('call fake_new')
        if new is object.__new__:
            return new(cls)
        return new(cls, *args, **kwargs)
        # here I get error: TypeError: object.__new__() takes exactly one argument (the type to instantiate)

    A.__new__ = fake_new
    try:
        yield
    finally:
        del A.__new__
        if new is not object.__new__:
            A.__new__ = new
        else:
            A.__new__ = lambda cls, *args, **kw: object.__new__(cls)

        print(A.__new__)

if __name__ == '__main__':
    A('foo')
    with patch_a():
        A('bar')
    A('baz')

(我尝试检查原始 __new__ 签名而不是 new is object.__new__ 比较 - 无济于事:object.__new__ 签名是 *args, **kwargs - 可能是为了使其在静态检查中永远不会失败)

【讨论】:

  • 我将def __new__(cls, *args, **kwargs): return super().__new__(cls) 添加到class A 并且一切正常。谢谢!
  • 啊,既然A.__new__被触动了,就算是原来的__new__也算是“自定义”了?伙计,这出乎意料
  • @Rugnar 所以,我认为,另一种方法是不保存对A.__new__ 的引用,然后在上下文管理器的末尾将其重新分配给A.__new__,而只是直接拨打object.__new__。不知道我更喜欢哪个
  • @juanpa.arrivillaga 在我的情况下,我需要同一个对象的多个嵌套补丁。所以调用 object.__new__ 对我不起作用。
  • @jsbueno 如果我想一想,它实际上是有道理的,它被认为是“触动”的方式是在 A 命名空间中是否存在 any __new__。最初,它只是简单地从object 继承,但是当您“取消修补”它时,它现在位于一个新的命名空间中(实际上,它在两个命名空间中)。有这样的效果
猜你喜欢
  • 2015-11-22
  • 2021-09-03
  • 2017-02-12
  • 1970-01-01
  • 2011-07-26
  • 2015-03-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多