Python修补__new__方法

7

我试图修补一个类的__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

但是在调用 fake_new 后,我得到了一个错误(请参见代码中的注释)。 对我来说,似乎我只是装饰了一个 __new__ 方法并传递了所有参数而没有改变它们。 这不起作用,原因对我来说很模糊。

此外,我可以写成 return new(cls) 并且调用 A('bar') 没有问题。但之后调用 A('baz') 就会出问题。

有人能解释一下发生了什么吗?

Python版本为3.8


A.__new__object.__new__,它只接受一个参数。你应该简单地调用它:new(cls)。我不明白为什么使用 'baz' 的调用会失败... 很奇怪... - juanpa.arrivillaga
1个回答

4
你遇到了Python对象实例化的复杂部分——在这个设计中,语言选择了一种方法,允许创建一个带有参数的自定义__init__方法,而无需触及__new__
然而,在类层次结构的基类object中,__new____init__都只需要一个单一的参数。
如果你的类有一个自定义的__init__,并且你没有触及__new__,同时还有更多的任何参数传递给类实例化,这些参数将从调用中剥离到__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 - 可能是这样做的,以便它永远不会在静态检查时失败。


我在A类中添加了def __new__(cls, *args, **kwargs): return super().__new__(cls),一切都正常工作了。谢谢! - Rugnar
2
啊,所以既然 A.__new__ 被修改了,即使它是原始的 __new__,它也被认为是“自定义”的?哎呀,这真是出乎意料。 - juanpa.arrivillaga
@Rugnar 所以,我认为另一种方法是不保存对A.__new__的引用,然后在上下文管理器结束时将其重新分配给A.__new__,而是直接调用object.__new__。 不确定哪个更好 - juanpa.arrivillaga
在我的情况下,我需要同一对象的多个嵌套补丁。因此,调用object.__new__对我来说行不通。@juanpa.arrivillaga - Rugnar
@jsbueno 如果我仔细想想,实际上这是有道理的——考虑到如果在A命名空间中存在任何 __new__,它就会被视为“已更改”。最初,它仅继承自“object”,但当你“取消打补丁”时,它现在位于一个新的命名空间中(确实,它同时存在于两个命名空间中)。大致是这样的。 - juanpa.arrivillaga
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接