【问题标题】:Unable to deepcopy a class with both __init__ and __new__ defined无法深度复制同时定义了 __init__ 和 __new__ 的类
【发布时间】:2020-01-16 02:55:34
【问题描述】:

我遇到了(在我看来)一个有点奇怪的问题。我已经定义了一个同时定义了 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'

这可能与 Decorating class with class wrapper and __new__ 相关 - 我不知道具体如何,但我在这里尝试类似的事情 - 我需要 new 将类包装器应用于我的测试实例已创建。

感谢您的帮助!

【问题讨论】:

    标签: python copy new-operator python-internals


    【解决方案1】:

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


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

    __deepcopy__ 没有在类上定义时,它属于这种情况:

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

    现在,reductor(4) 在这里返回 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 为 &lt;class '__main__.Test'&gt;,因此您会收到错误消息。


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

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

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

    使用_PyObject_GetNewArguments 找出参数本身。

    现在这个函数在类上查找__getnewargs_ex____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
    

    令人惊讶的是,即使我们是从 __getnewargs__ 中返回的,xx 的深层副本 Eggs 也没有存储在 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)  <---
        ...
    

    还有其他方法吗?

    注意上面的解释,工作功能只是为了解释问题。我不会真的把它称为最好或更坏的方法。

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

    【讨论】:

    • 谢谢 - 这提高了我的理解。因此,如果一个类同时具有 newinit 方法,则会出现问题,因为 _PyObject_GetNewArguments 检查 new 的参数,而不是 初始化?出于好奇,对于 deepcopy 来说,检查 initnew 参数并聚合它们是否会更明智?
    • 不,_PyObject_GetNewArguments 根本没有调查__init____new__ 来找出论据,正如it checks whether you've defined __getnewargs_ex__ or __getnewargs__ 所述。
    • 可以检查__new____init__的签名,甚至还有builtin lib。但真正的问题是找出为参数传递的值,因为它需要找出在哪里以及如何调用Test,不幸的是,这在 Python 中很难确定。
    • 当然——所以可以说问题的根源在于 deepcopy 无法追溯确定参数是如何在 new 和 init 之间分配的——因此无法正确重建原始对象.
    • @Chrisper 换句话说,它不知道如何实例化Test,除非你明确告诉它如何实例化。
    【解决方案2】:

    好的 - 这是因为我做错了 - 我不应该从 new 显式调用 init。罪魁祸首。

    【讨论】:

      猜你喜欢
      • 2011-01-02
      • 2012-08-01
      • 2018-11-11
      • 2019-10-24
      • 1970-01-01
      • 2012-10-09
      • 2015-10-24
      • 2016-11-06
      • 2011-04-04
      相关资源
      最近更新 更多