pickle与deepcopy的关系

33 投票
2 回答
12125 浏览
提问于 2025-04-17 22:11

那么,picklecopy.deepcopy之间到底是什么关系呢?它们共享了哪些机制,又是怎么回事呢?

很明显,这两个操作是紧密相关的,并且共享了一些机制或协议,但我对具体细节还是有些搞不清楚。

我发现了一些(让人困惑的)事情:

  1. 如果一个类定义了 __[gs]etstate__ 方法,那么在对它的实例进行 deepcopy 时,这些方法会被调用。起初我对此感到惊讶,因为我以为这些方法只和 pickle 有关,但后来我发现,类可以使用与控制复制相同的接口来控制序列化。不过,关于 如何 在进行深拷贝时使用 __[gs]etstate__ 的文档并不多(比如从 __getstate__ 返回的值是怎么用的,传给 __setstate__ 的是什么?)
  2. 一个简单的 deepcopy 实现方式是 pickle.loads(pickle.dumps(obj))。但是,这种方式不可能等同于真正的深拷贝,因为如果一个类定义了 __deepcopy__ 方法,这个方法在使用基于 pickle 的深拷贝实现时不会被调用。(我还看到过一个说法,深拷贝比 pickle 更通用,有很多类型可以进行深拷贝,但不能被 pickle 序列化。)

第一点说明了它们之间的共同点,而第二点则说明了 pickledeepcopy 之间的不同。

此外,我还发现了这两个相互矛盾的说法:

copy_reg:在对这些对象进行序列化或复制时,picklecPicklecopy 模块会使用这些函数。

copy 模块并不使用 copy_reg 注册模块。

这从一个方面来说又是 pickledeepcopy 之间关系的另一个迹象,但另一方面,也让我更加困惑……

[我的经验是基于 python2.7,但我也希望能得到关于 python2 和 python3 之间在 pickledeepcopy 的差异的任何指点]

2 个回答

8

好的,我为了这个问题查了一下源代码,结果发现其实答案挺简单的。

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 中,应该能让 copydeepcopy 正常工作。

14

你不需要对(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__来创建给定对象派生类的新实例。

撰写回答