copy.deepcopy 与 pickle

33 投票
5 回答
17956 浏览
提问于 2025-04-15 14:15

我有一个小部件的树形结构,比如一个集合里面包含模型,而模型里面又包含小部件。我想复制整个集合,发现使用copy.deepcopy比用'pickle'和'de-pickle'来处理对象要快,但因为cPickle是用C语言写的,所以速度更快。

  1. 那我(我们)为什么不总是使用cPickle而不是deepcopy呢?
  2. 还有其他的复制方法吗?因为pickle比deepcopy慢,但cPickle又更快,所以也许用C语言实现的deepcopy会更好。

示例测试代码:

import copy
import pickle
import cPickle

class A(object): pass

d = {}
for i in range(1000):
    d[i] = A()

def copy1():
    return copy.deepcopy(d)

def copy2():
    return pickle.loads(pickle.dumps(d, -1))

def copy3():
    return cPickle.loads(cPickle.dumps(d, -1))

时间记录:

>python -m timeit -s "import c" "c.copy1()"
10 loops, best of 3: 46.3 msec per loop

>python -m timeit -s "import c" "c.copy2()"
10 loops, best of 3: 93.3 msec per loop

>python -m timeit -s "import c" "c.copy3()"
100 loops, best of 3: 17.1 msec per loop

5 个回答

2

并不是说 cPickle 总是比 deepcopy() 快。虽然 cPickle 通常比 pickle 快,但它是否比 deepcopy 快,取决于以下几个因素:

  • 要复制的结构的大小和嵌套层级,
  • 包含对象的类型,以及
  • 被序列化成字符串后的大小。

如果某个东西可以被序列化(pickled),那么它显然也可以被深拷贝(deepcopied),但反过来就不一定了:要序列化某个东西,它需要被完全转换成一种可存储的格式; 而深拷贝并不需要这样。特别是,你可以通过在内存中复制一个结构(想象一下扩展类型)来非常高效地实现 __deepcopy__,而不需要把所有东西都保存到磁盘上。(想想将数据挂起到内存和挂起到磁盘的区别。)

一个满足上述条件的著名扩展类型是 ndarray,实际上,它是你观察结果的一个很好的反例:使用 d = numpy.arange(100000000),你的代码会给出不同的运行时间:

In [1]: import copy, pickle, cPickle, numpy

In [2]: d = numpy.arange(100000000)

In [3]: %timeit pickle.loads(pickle.dumps(d, -1))
1 loops, best of 3: 2.95 s per loop

In [4]: %timeit cPickle.loads(cPickle.dumps(d, -1))
1 loops, best of 3: 2.37 s per loop

In [5]: %timeit copy.deepcopy(d)
1 loops, best of 3: 459 ms per loop

如果没有实现 __deepcopy__,那么 copypickle 共享相同的基础设施(参见 copy_reg 模块,讨论内容可以在 pickle 和 deepcopy 之间的关系 中找到)。

8

你应该使用深拷贝,因为这样可以让你的代码更容易理解。如果用一种序列化的方法来复制内存中的对象,至少会让其他阅读你代码的开发者感到困惑。而使用深拷贝还意味着你可以享受到未来深拷贝可能带来的优化好处。

优化的第一条规则是:别优化。

37

问题是,使用pickle和unpickle的速度可能更快(因为它是用C语言实现的),这是因为它的功能不如深拷贝通用:很多对象可以进行深拷贝,但不一定能被pickle化。举个例子,如果你的类A被改成了...:

class A(object):
  class B(object): pass
  def __init__(self): self.b = self.B()

那么,copy1依然可以正常工作(虽然A的复杂性让它变慢,但并不会导致失败);而copy2copy3就会出问题,堆栈跟踪的最后部分显示...:

  File "./c.py", line 20, in copy3
    return cPickle.loads(cPickle.dumps(d, -1))
PicklingError: Can't pickle <class 'c.B'>: attribute lookup c.B failed

也就是说,pickle化总是认为类和函数是在它们模块中的顶层实体,因此是通过“名称”来pickle它们的——而深拷贝则没有这样的假设。

所以,如果你遇到一种情况,速度要求非常高,“某种程度的深拷贝”是绝对关键的,每一毫秒都很重要,并且你想利用一些你知道适用于你正在复制的对象的特殊限制,比如那些使得pickle化适用的限制,或者其他序列化和快捷方式的优势,那么你可以这样做——但如果这样做,你必须意识到你是在让你的系统永远受这些限制的约束,并且要非常清楚和明确地记录下这个设计决定,以便将来维护的人能理解。

对于正常情况,如果你想要通用性,就用deepcopy吧!-)

撰写回答