pickle与deepcopy的关系
那么,pickle
和copy.deepcopy
之间到底是什么关系呢?它们共享了哪些机制,又是怎么回事呢?
很明显,这两个操作是紧密相关的,并且共享了一些机制或协议,但我对具体细节还是有些搞不清楚。
我发现了一些(让人困惑的)事情:
- 如果一个类定义了
__[gs]etstate__
方法,那么在对它的实例进行deepcopy
时,这些方法会被调用。起初我对此感到惊讶,因为我以为这些方法只和pickle
有关,但后来我发现,类可以使用与控制复制相同的接口来控制序列化。不过,关于 如何 在进行深拷贝时使用__[gs]etstate__
的文档并不多(比如从__getstate__
返回的值是怎么用的,传给__setstate__
的是什么?) - 一个简单的
deepcopy
实现方式是pickle.loads(pickle.dumps(obj))
。但是,这种方式不可能等同于真正的深拷贝,因为如果一个类定义了__deepcopy__
方法,这个方法在使用基于pickle
的深拷贝实现时不会被调用。(我还看到过一个说法,深拷贝比pickle
更通用,有很多类型可以进行深拷贝,但不能被pickle
序列化。)
第一点说明了它们之间的共同点,而第二点则说明了 pickle
和 deepcopy
之间的不同。
此外,我还发现了这两个相互矛盾的说法:
copy_reg:在对这些对象进行序列化或复制时,
pickle
、cPickle
和copy
模块会使用这些函数。
和
copy
模块并不使用copy_reg
注册模块。
这从一个方面来说又是 pickle
和 deepcopy
之间关系的另一个迹象,但另一方面,也让我更加困惑……
[我的经验是基于 python2.7,但我也希望能得到关于 python2 和 python3 之间在 pickle
和 deepcopy
的差异的任何指点]
2 个回答
好的,我为了这个问题查了一下源代码,结果发现其实答案挺简单的。
copy
这个功能会查看一些它知道的内置类型的构造方法,这些构造方法被记录在一个叫 _copy_dispatch
的字典里。当它不知道怎么复制某种基本类型时,它会引入 copy_reg.dispatch_table
,这是一个地方,pickle
在这里注册它知道的生成新对象的方法。简单来说,这就像是一个字典,里面记录了对象的类型和“生成新对象的函数”——这个“生成新对象的函数”基本上就是你在为一个对象写 __reduce__
或 __reduce_ex__
方法时所写的内容。如果其中一个方法缺失或者需要帮助,它会转而使用 __setstate__
、__getstate__
等方法。
所以这就是 copy
的工作原理。基本上……(还有一些额外的情况……)
def copy(x):
"""Shallow copy operation on arbitrary Python objects.
See the module's __doc__ string for more info.
"""
cls = type(x)
copier = _copy_dispatch.get(cls)
if copier:
return copier(x)
copier = getattr(cls, "__copy__", None)
if copier:
return copier(x)
reductor = dispatch_table.get(cls)
if reductor:
rv = reductor(x)
else:
reductor = getattr(x, "__reduce_ex__", None)
if reductor:
rv = reductor(2)
else:
reductor = getattr(x, "__reduce__", None)
if reductor:
rv = reductor()
else:
raise Error("un(shallow)copyable object of type %s" % cls)
deepcopy
的工作原理和上面的一样,但它会检查每个对象,确保每个新对象都有一个独立的副本,而不是指向原对象的引用。deepcopy
会建立自己的 _deepcopy_dispatch
表(一个字典),在这里注册一些函数,以确保生成的新对象不会指向原对象(这些原对象可能是通过在 copy_reg.dispatch_table
中注册的 __reduce__
函数生成的)。
因此,编写一个 __reduce__
方法(或类似的方法)并将其注册到 copy_reg
中,应该能让 copy
和 deepcopy
正常工作。
你不需要对(1)和(2)感到困惑。一般来说,Python会尝试为缺失的方法提供合理的替代方案。比如,只要定义了__getitem__
,就可以让一个类变得可迭代,但如果同时实现__iter__
,可能会更高效。类似的情况也适用于像__add__
这样的操作,__iadd__
等是可选的。
__deepcopy__
是deepcopy()
会寻找的最专业的方法,但如果这个方法不存在,使用pickle协议作为替代是个明智的选择。它实际上并不会调用dumps()
或loads()
,因为它不依赖于中间表示是字符串,而是会间接使用__getstate__
和__setstate__
(通过__reduce__
),正如你观察到的那样。
目前,文档仍然说明
… 复制模块并不使用copy_reg注册模块。
但这似乎是一个已经修复的bug(可能是2.7版本在这方面没有得到足够的关注)。
还要注意,这个功能在Python中是相当深入的整合(至少在现在是这样);object
类本身实现了__reduce__
(以及它的版本化变体_ex),这个方法会引用copy_reg.__newobj__
来创建给定对象派生类的新实例。