无法深度复制同时定义了 __init__ 和 __new__ 的 class

Unable to deepcopy a class with both __init__ and __new__ defined

我遇到了(在我看来)一个有点奇怪的问题。我定义了一个 class,同时定义了 initnew,如下:

class Test:

    def __init__(self, num1):
        self.num1 = num1

    def __new__(cls, *args, **kwargs):
        new_inst = object.__new__(cls)
        new_inst.__init__(*args, **kwargs)
        new_inst.extra = 2
        return new_inst

如果正常使用,这很好用:

test = Test(1)
assert test.extra == 2

但是不会copy.deepcopy:

import copy
copy.deepcopy(test)

给予

TypeError: __init__() missing 1 required positional argument: 'num1'

这可能与 有关 - 我看不出具体如何,但我正在尝试类似的事情 - 我需要 new 来应用 class 包装到我创建的测试实例。

感谢收到任何帮助!

好的 - 这是因为我做错了 - 我不应该明确地从 new 调用 init。过错了。

从技术上讲,从 __new__ 调用 __init__ 不是问题,但它是多余的,因为一旦 __new__ returns 调用 __init__ 就会自动发生实例.


现在来看看 deepcopy 失败的原因,我们可以稍微研究一下 its internals

__deepcopy__ 未在 class 上定义时,它属于这种情况:

reductor = getattr(x, "__reduce_ex__", None)
rv = reductor(4)

现在,这里 reductor(4) returns function to be used to re-create the object,对象的类型(Test),要传递的参数及其状态(在本例中为实例字典中的项目 test.__dict__):

>>> !rv
(
    <function __newobj__ at 0x7f491938f1e0>,  # func
    (<class '__main__.Test'>,),  # type + args in a single tuple
    {'num1': 1, 'extra': []}, None, None) # state

现在它用这个数据调用 _reconstruct

def _reconstruct(x, memo, func, args,
                 state=None, listiter=None, dictiter=None,
                 deepcopy=deepcopy):
    deep = memo is not None
    if deep and args:
        args = (deepcopy(arg, memo) for arg in args)
    y = func(*args)
    ...

这里这个调用最终会调用:

def __newobj__(cls, *args):
    return cls.__new__(cls, *args)

但是因为 args 是空的并且 cls 是 <class '__main__.Test'>,所以你得到了错误。


现在 Python 如何为您的对象决定这些参数,因为这似乎是问题所在?

为此,我们需要查看:reductor(4),其中 reductor 是 __reduce_ex__,此处传递的 4 是 pickle 协议版本。

现在这个 __reduce_ex__ 内部调用 reduce_newobj 来获取对象创建函数、参数、状态等以创建新副本。

参数本身是用_PyObject_GetNewArguments找到的。

现在这个函数在 class 上寻找 __getnewargs_ex____getnewargs__,因为我们的 class 没有,所以我们没有得到任何参数。


现在让我们添加此方法并重试:

import copy


class Test:

    def __init__(self, num1):
        self.num1 = num1

    def __getnewargs__(self):
        return ('Eggs',)

    def __new__(cls, *args, **kwargs):
        print(args)
        new_inst = object.__new__(cls)
        new_inst.__init__(*args, **kwargs)
        new_inst.extra = []
        return new_inst

test = Test([])

xx = copy.deepcopy(test)

print(xx.num1, test.num1, id(xx.num1), id(test.num1))

# ([],)
# ('Eggs',)
# [] [] 139725263987016 139725265534088

令人惊讶的是,即使我们从 __getnewargs__ 返回深层副本 xxEggs 也没有存储在 num1 中。这是因为函数 _reconstruct 在实例创建后将其最初获得的状态的深层副本重新添加到实例中,从而覆盖了这些更改。


def _reconstruct(x, memo, func, args,
                 state=None, listiter=None, dictiter=None,
                 deepcopy=deepcopy):
    deep = memo is not None
    if deep and args:
        args = (deepcopy(arg, memo) for arg in args)
    y = func(*args)
    if deep:
        memo[id(x)] = y

    if state is not None:
        ...
            if state is not None:
                y.__dict__.update(state)  <---
    ...

还有其他方法吗?

注意上面的解释,工作函数只是为了说明问题。我不会真的称之为最好或更差的方法。

是的,您可以在 class 上定义自己的 __deepcopy__ 挂钩以进一步控制行为。我会把这个留给用户练习。