最近在用一个Python项目,发现Python的深拷贝copy.deepcopy实在是太慢了。相关背景在Python中,我们有两种复制对象的方式:浅拷贝和深拷贝。在copy模块中可以找到浅拷贝和深拷贝,其中copy.copy()执行的是浅拷贝操作,copy.deepcopy()执行的是深拷贝操作。浅拷贝是在另一个地址创建一个新对象,但新对象中的子对象引用指向源对象的子对象。如果此时我们修改源对象的子对象的属性,那么新对象中的子对象的属性也会发生变化。为了得到一个与源对象没有关系的全新对象,我们需要一个深拷贝。深拷贝就是在另一个地址新建一个对象,同时对象中的子对象也新开,值和源对象一样。下面用一个例子来说明浅拷贝和深拷贝的区别。以下代码将输出“b[2,2,3]”和“c[1,2,3]”。importcopyclassA:def__init__(self):array=[1,2,3]a=A()b=copy.copy(a)c=copy.deepcopy(a)a.array[0]=2print"b",b.arrayprint"c",c.arrayb来自浅拷贝,c来自深拷贝。修改a.array后,b.array也随之改变。实际上a.array和b.array指向同一个对象。c.array保持原样。深拷贝比浅拷贝更符合人的直觉,但代价是深拷贝太慢了。在下面的代码中,case1和case2是等价的。在我的机器上测试,case1用时不到一秒,而case2达到十秒。那么为什么深拷贝这么慢呢?importcopyclassA:def__init__(self):array=[1,2,3]a=[A()foriinxrange(100000)]a[10].array[0]=100###case1b=[A()foriinxrange(100000))]foriinxrange(100000):forjinxrange(3):b[i].array[j]=a[i].array[j]###case2c=copy.deepcopy(a)深拷贝分析为了找出为什么Python深拷贝这么慢,我看了Python2.7中copy.deepcopy的实现:https://github.com/python/cpython/blob/2.7/Lib/copy.py。它的一般结构是:defdeepcopy(x,memo=None,_nil=[]):ifmemoisNone:memo={}d=id(x)y=memo.get(d,_nil)ifyisnot_nil:returnycls=type(x)copier=_deepcopy_dispatch.get(cls)ifcopier:y=copier(x,memo)else:...##其他特殊复制方法memo[d]=yreturny其中memo保存所有复制的对象。为什么要设置备忘录?在一些特殊情况下,一个对象的相关对象可以指向它自己,比如双向链表。如果不保存复制的对象,程序就会陷入死循环。还有一点值得注意的是_deepcopy_dispatch,这行代码根据要复制的对象类型选择对应的复制函数。可选的拷贝函数有:拷贝基本数据类型的_deepcopy_atomic、拷贝列表的_deepcopy_list、拷贝元组的_deepcopy_tuple、拷贝字典的_deepcopy_dict、拷贝自定义对象的_deepcopy_inst等。比较重要的拷贝函数__deepcopy_inst的代码如下:如果object有一个__deepcopy__函数,使用这个函数进行复制;如果不是,先复制初始构造参数,然后构造一个对象,再复制对象状态。def_deepcopy_inst(x,备忘录):ifhasattr(x,'__deepcopy__'):returnx.__deepcopy__(memo)ifhasattr(x,'__getinitargs__'):args=x.__getinitargs__()args=deepcopy(args,memo)y=x.__class__(*args)else:y=_EmptyClass()y.__class__=x.__class__memo[id(x)]=yifhasattr(x,'__getstate__'):state=x.__getstate__()else:state=x.__dict__state=deepcopy(state,memo)ifhasattr(y,'__setstate__'):y.__setstate__(state)else:y.__dict__.update(state)returnydeepcopy需要维护一个memo来记录复制的对象,这是它的比较缓慢的原因。在绝大多数情况下,程序中没有交叉引用。但是作为一个通用的模块,Python深拷贝必须为了这1%的情况牺牲99%的性能。在一种场景下,上面给出了深拷贝效率对比的结果。可以看出深拷贝很慢,但是没有办法直观的感受到深拷贝拖累了整个程序的运行速度。下面是一个实际项目的例子,是我最近在写的一些游戏环境。玩家程序获取游戏环境给定的信息,当前玩家程序选择合适的动作,游戏环境根据动作推进游戏逻辑;重复上述过程,直至决出胜负;整个过程如下图。玩家程序的信息必须从游戏环境中深度复制。如果给予玩家的信息是游戏环境的浅拷贝,玩家程序就有可能通过这些信息获取游戏秘密或操纵游戏。我们已经知道默认的深拷贝很慢。为了提高深拷贝的效率,我们在信息类和相关类中加入了__deepcopy__功能。以下是信息类的示例。classFiveCardStudInfo(roomai.abstract.AbstractInfo):public_state=Noneperson_state=Nonedef__deepcopy__(self,memodict={}):info=FiveCardStudInfo()info.public_state=self.public_state.__deepcopy__()info.public_state=self.deepcopy__(__person_)返回信息简单地进行了一个效率对比实验:让随机玩家玩一千轮梭哈,统计耗时。实验结果如下所示。使用原始深拷贝,程序运行时间为143秒,其中深拷贝时间为134秒,深拷贝时间占整个程序时间的94%。使用改进后的深拷贝,程序运行时间为23秒,其中深拷贝时间为16秒,占比69%。尽管深拷贝仍然占到69%,但相比94%下降了很多。整体程序运行时间也从134秒减少到23秒。综上所述,Python的深拷贝非常慢,因为深拷贝需要维护一个memo来记录拷贝的对象。之所以维护这个备忘录,是为了避免相互引用造成的死循环。但是作为一个通用的模块,Python深拷贝必须为了这1%的情况牺牲99%的性能。我们可以通过自己实现__deepcopy__函数来提高它的效率。【本文为专栏作家“李莉”原创稿件,转载请联系授权】点此查看该作者更多好文
