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

Chr*_*per 2 python copy new-operator python-internals

我遇到了(在我看来)一个有点奇怪的问题。我定义了一个同时定义了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
Run Code Online (Sandbox Code Playgroud)

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

test = Test(1)
assert test.extra == 2
Run Code Online (Sandbox Code Playgroud)

但是,它不会复制。deepcopy:

import copy
copy.deepcopy(test)
Run Code Online (Sandbox Code Playgroud)

给出

TypeError: __init__() missing 1 required positional argument: 'num1'
Run Code Online (Sandbox Code Playgroud)

这可能与使用类包装器和 __new__ 装饰类有关- 我无法确切地看到如何,但我在这里尝试类似的事情 - 我需要new将类包装器应用到我创建的测试实例。

任何帮助感激不尽!

Ash*_*ary 5

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


现在来看看为什么会失败,我们可以稍微deepcopy研究一下它的内部结构。

__deepcopy__类上没有定义时,它会遇到这种情况:

reductor = getattr(x, "__reduce_ex__", None)
rv = reductor(4)
Run Code Online (Sandbox Code Playgroud)

现在,这里reductor(4)返回用于重新创建对象的函数、对象的类型(Test)、要传递的参数及其状态(在本例中为实例字典中的项目test.__dict__):

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

现在它_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)
    ...
Run Code Online (Sandbox Code Playgroud)

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

def __newobj__(cls, *args):
    return cls.__new__(cls, *args)
Run Code Online (Sandbox Code Playgroud)

但由于args是 空且 cls 是<class '__main__.Test'>,因此您会收到错误。


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

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

现在,它__reduce_ex__在内部调用reduce_newobj以获取要制作的新副本的对象创建函数、参数、状态等。

参数本身是使用 找到的_PyObject_GetNewArguments

现在这个函数在类上查找__getnewargs_ex__or __getnewargs__,因为我们的类没有它,所以我们没有得到任何参数。


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

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
Run Code Online (Sandbox Code Playgroud)

令人惊讶的xx是,Eggs即使num1我们从__getnewargs__. 这是因为该函数_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)  <---
    ...
Run Code Online (Sandbox Code Playgroud)

还有其他方法吗?

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

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