无法深度复制同时定义了 __init__ 和 __new__ 的 class
Unable to deepcopy a class with both __init__ and __new__ defined
我遇到了(在我看来)一个有点奇怪的问题。我定义了一个 class,同时定义了 init 和 new,如下:
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__
返回深层副本 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) <---
...
还有其他方法吗?
注意上面的解释,工作函数只是为了说明问题。我不会真的称之为最好或更差的方法。
是的,您可以在 class 上定义自己的 __deepcopy__
挂钩以进一步控制行为。我会把这个留给用户练习。
我遇到了(在我看来)一个有点奇怪的问题。我定义了一个 class,同时定义了 init 和 new,如下:
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 调用 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__
返回深层副本 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) <---
...
还有其他方法吗?
注意上面的解释,工作函数只是为了说明问题。我不会真的称之为最好或更差的方法。
是的,您可以在 class 上定义自己的 __deepcopy__
挂钩以进一步控制行为。我会把这个留给用户练习。