深拷贝(deepcopy())非常慢

55

我在Python中有一个游戏状态,大约有1000个对象(行星系统 + 星星 + 行星),当被请求时需要复制并应用一系列转换。然而,以每秒1个请求为基础,这占用了我的运行时间的24.63%,如图示。该如何加快速度?请注意,由于转换涉及几乎所有内容,因此无法选择少复制。

编辑:通过对事物实施谨慎的__deepcopy__实现,将其缩减至8%。但仍不够好(足够好是1%或更少,我计划向其添加更多内容)。timeit显示每个deepcopy()花费41.8毫秒。


与其复制状态,你可以创建一个操作队列:使用当前状态确定所有对象的下一步动作和效果,而不立即应用它们,然后批量应用所有这些动作,然后计算下一个“回合”的动作等。 - tobias_k
@tobias_k 这实际上正是我的变换堆栈正在做的事情,但我经常需要重新开始,因此需要复制。 - Electro
是的,但是如果要在不修改初始状态的情况下访问输出,我仍然需要一个副本。 - Electro
为什么不使用执行浅复制的'copy()'? 我不明白你何时需要深拷贝。 - Antoine
@tobias_k 这可能有效,但维护内部引用会需要很多工作。 - Electro
显示剩余5条评论
5个回答

71

实际上,深度复制非常慢。但我们可以使用json、ujson或cPickle。 我们可以使用json/cPickle将对象转储,然后稍后再加载它。 这是我的测试:

Total time: 3.46068 s
File: test_deepcopy.py
Function: test at line 15
Line #   Hits          Time Per Hit   % Time  Line Contents
==============================================================
15                                             @profile
16                                             def test():
17       100       957585   9575.9     27.7        b = deepcopy(a)
18       100          862      8.6      0.0        c = copy(a)
19       100        42295    422.9      1.2        d = ujson.loads(ujson.dumps(a))
20       100        85040    850.4      2.5        e = json.loads(json.dumps(a))
21       100      2323465  23234.7     67.1        f = pickle.loads(pickle.dumps(a, -1))
22       100        51434    514.3      1.5        g = cPickle.loads(cPickle.dumps(a, -1))

从我们所看到的情况来看,json / ujson / cPickle比deepcopy更快,但pickle...


1
这很可怕。deepcopy有什么作用是cPickle没有的吗? - Electro
2
我不知道... 你可以在这里找到一些有用的东西:https://docs.python.org/2/library/copy.html :) - cherish
6
经过测试发现,在字典列表方面,深拷贝的性能优于 JSON,而对于大型嵌套字典,JSON 的性能优于深拷贝。 - wrkyle
11
请注意,在Python3中,pickle 对应的是这里列出的 cPickle - Karl Bartel
可怕的是 deepcopy 所花费的时间。我已经在一个回溯算法中检查了 pickle(Python3),它运行得非常完美。 - Ivan
4
小心使用json.loads(json.dumps(a))以及可能也包括ujson,它们会把所有的键都视为字符串。如果一个键不是字符串类型,它会被转换为字符串。 - Rikard Olsson

10
如果你创建自己的类来保存这些对象,你可以创建自己的方法来处理复制和深度复制。http://www.rafekettler.com/magicmethods.html#copying(链接已经失效)
新的github仓库链接:https://github.com/RafeKettler/magicmethods
class MyClass():
    def __copy__(self):
        copy_object = MyClass()
        return copy_object

    def __deepcopy__(self, memodict={}):
        copy_object = MyClass()
        copy_object.value = self.value
        return copy_object

if __name__ == "__main__":
    my_inst = MyClass()
    print(copy.deepcopy(my_inst))

这是先前破损链接中相似的描述。

复制

有时,特别是在处理可变对象时,您希望能够复制一个对象并进行更改,而不会影响从中复制的内容。这就是Python的copy发挥作用的地方。但是,幸运的是,Python模块没有感性,因此我们不必担心基于Linux的机器人起义,但我们确实必须告诉Python如何有效地复制东西。

__copy__(self)

为您的类的实例定义了copy.copy()的行为。 copy.copy()返回对象的浅拷贝--这意味着尽管实例本身是一个新实例,但其所有数据都被引用--即对象本身被复制,但其数据仍被引用(因此对浅拷贝中的数据的更改可能会导致原始数据的更改)。

__deepcopy__(self, memodict={})

为您的类的实例定义了copy.deepcopy()的行为。 copy.deepcopy()返回对象的深层副本--对象及其数据都被复制。memodict是以前复制的对象的缓存--这优化了复制并防止复制递归数据结构时的无限递归。当您想要对单个属性进行深层复制时,请调用那个属性上的copy.deepcopy(),并将memodict作为第一个参数。这些魔术方法的一些用例是什么?像往常一样,在任何情况下,如果您需要比默认行为更精细的控制,则需要这样做。例如,如果您尝试复制将缓存存储为字典的对象(该字典可能很大),则也许不应将缓存复制--如果缓存可以在实例之间在内存中共享,则应该这样做。


这实际上是我刚刚追求的。然而,我只将其降低到了运行时间的8%。学到的东西:deepcopy一个sortedcontainers.SortedListWithKey很慢,先将其转换为列表。同时复制itertools.count()也很慢,这可能适用于生成器。 - Electro
我找到了一个类似的复制描述,并将其添加到了我的答案底部。我还发现该链接有一个Github存储库。 - justengel

4

我做了一个快速实验,比较了深拷贝(deepcopy)、json和ujson在几种情况下的表现,我的结果与@cherish在某些情况下的结果相矛盾,将这个小实验发布在这里:

import ujson
import timeit
import json
import random
import string
import copy
import ujson
import sys


def random_string(N):
    return ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(N))


def random_json(width=5, height=5, levels=1):
    dct = {}
    lst = [random_string(4) for i in range(width)]
    lst2 = [random.randint(0, 10000) for i in range(width)]
    lst3 = [bool(random.randint(0, 1)) for i in range(width)]
    for j in range(height):
        dct[str(j)] = lst
        dct[str(width+j)] = lst2
        dct[str(2*width+j)] = lst3

    for i in range(levels):
        new_dct = {}
        for j in range(height):
            new_dct[str(j)] = dct
        dct = json.loads(json.dumps(new_dct))

    return new_dct

if __name__ == "__main__":
    print(sys.version)
    levels = 3
    for i in range(15):
        dataset = random_json(i, i, levels)
        print("Comparing deepcopy/ujson/json using random dataset({},{},{}), length {}".format(i,i,levels, len(json.dumps(dataset))))
        print(timeit.timeit('copy.deepcopy(dataset)',
                            setup='from __main__ import copy, dataset', number=10))
        print(timeit.timeit('ujson.loads(ujson.dumps(dataset))',
                            setup='from __main__ import ujson, dataset', number=10))
        print(timeit.timeit('json.loads(json.dumps(dataset))',
                            setup='from __main__ import json, dataset', number=10))
        print()

结果将是:
3.6.4 (v3.6.4:d48eceb, Dec 19 2017, 06:04:45) [MSC v.1900 32 bit (Intel)]
Comparing deepcopy/ujson/json using random dataset(0,0,3), length 2
2.6842977659931844e-05
0.00012039864979822371
7.776568527950847e-05

Comparing deepcopy/ujson/json using random dataset(1,1,3), length 63
0.0002731667726569534
3.552747043226263e-05
0.00012987264191349377

Comparing deepcopy/ujson/json using random dataset(2,2,3), length 1106
0.0011858280130946362
0.00034974820892205325
0.0007093651596308467

Comparing deepcopy/ujson/json using random dataset(3,3,3), length 6834
0.0042218477363672215
0.0021178319874343293
0.003378267688436718

Comparing deepcopy/ujson/json using random dataset(4,4,3), length 26572
0.011379054029782284
0.006288757016181971
0.009920059244030693

Comparing deepcopy/ujson/json using random dataset(5,5,3), length 79210
0.028879491215043435
0.027906433274870912
0.029595961868760734

Comparing deepcopy/ujson/json using random dataset(6,6,3), length 183678
0.047142979515255284
0.04682125853300759
0.06791747047568517

Comparing deepcopy/ujson/json using random dataset(7,7,3), length 395528
0.08239215142913198
0.09871347134571351
0.15347433002098887

Comparing deepcopy/ujson/json using random dataset(8,8,3), length 764920
0.1351954464835896
0.19448842613700734
0.3020533693660834

Comparing deepcopy/ujson/json using random dataset(9,9,3), length 1356570
0.24560258734724671
0.44074906118659407
0.5705849913806413

Comparing deepcopy/ujson/json using random dataset(10,10,3), length 2287770
0.3237815755327835
0.61104051671153
0.8698565598118777

Comparing deepcopy/ujson/json using random dataset(11,11,3), length 3598750
0.4958284828467452
0.9472223636741877
1.2514314609961668

Comparing deepcopy/ujson/json using random dataset(12,12,3), length 5636414
0.6261448233909714
1.4066722957969802
1.8636325417418167

Comparing deepcopy/ujson/json using random dataset(13,13,3), length 8220800
0.8396582099444547
2.061675688670409
2.755659427352441

Comparing deepcopy/ujson/json using random dataset(14,14,3), length 12018290
1.0951926990258762
2.96703050743886
4.088875914783021

这个小实验的结论如下:
  • 当字典比较小时,time(ujson)<time(json)<time(deepcopy)
  • 当字典比较大时,time(deepcopy)<time(ujson)<time(json)
因此,每秒需要复制的数量和您处理的字典类型取决于您更喜欢使用深层复制还是ujson。

4

内置模块 marshal 比 ujson 更快,支持更多的原生类型,如集合、元组和非字符串字典键。它在不同的 Python 版本之间不可移植(类似于 pickle),但对于本地克隆数据来说,这不是个问题。

为了展示这一点,我使用了@BPL在另一个答案中的测试程序并在比较的情况下添加了 marshal,并在一个兼容 ARMv6 的处理器上运行了它。

新增案例:

print(timeit.timeit('marshal.loads(marshal.dumps(dataset))',
       setup='from __main__ import marshal, dataset', number=1))

结果(对于所有情况,Marshal是最快的):
2.7.14 (default, Mar  6 2019, 13:27:55)
[GCC 7.3.0]
Comparing deepcopy/marshal/ujson/json using random dataset(0,0,1), length 2
0.000588178634644
0.000134944915771
0.000258922576904
0.00113606452942
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,0,3), length 2
0.000546932220459
0.000134944915771
0.000180006027222
0.00120401382446
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,0,5), length 2
0.000545978546143
0.000128984451294
0.000185966491699
0.00106000900269
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,2,1), length 50
0.00154900550842
0.000281810760498
0.000414848327637
0.00174903869629
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,2,3), length 242
0.00655102729797
0.000789880752563
0.00133085250854
0.00432300567627
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,2,5), length 1010
0.0514280796051
0.0015549659729
0.00413513183594
0.0148711204529
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,4,1), length 172
0.00250005722046
0.000365018844604
0.000761985778809
0.00263404846191
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,4,3), length 2892
0.0329101085663
0.00363397598267
0.0110101699829
0.0262169837952
()
Comparing deepcopy/marshal/ujson/json using random dataset(0,4,5), length 46412
0.616458892822
0.0826110839844
0.189103841782
0.504135131836
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,0,1), length 2
0.000693082809448
0.000132083892822
0.000182867050171
0.00107002258301
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,0,3), length 2
0.000566005706787
0.000132083892822
0.000180959701538
0.00107598304749
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,0,5), length 2
0.000562906265259
0.000128984451294
0.000184059143066
0.00118517875671
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,2,1), length 258
0.00405406951904
0.000534057617188
0.00124287605286
0.00309610366821
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,2,3), length 1058
0.026270866394
0.00180387496948
0.00363302230835
0.0096640586853
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,2,5), length 4338
0.0778729915619
0.00682806968689
0.0151469707489
0.0468928813934
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,4,1), length 716
0.00720596313477
0.00100684165955
0.0215280056
0.0062358379364
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,4,3), length 11468
0.112984895706
0.0238728523254
0.0448131561279
0.0874760150909
()
Comparing deepcopy/marshal/ujson/json using random dataset(2,4,5), length 183628
1.83552503586
0.407335042953
0.617804050446
1.65498495102
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,0,1), length 2
0.000571012496948
0.000132083892822
0.000189781188965
0.00121593475342
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,0,3), length 2
0.000757932662964
0.000131130218506
0.000180959701538
0.00144195556641
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,0,5), length 2
0.00056791305542
0.000132083892822
0.000184059143066
0.00107407569885
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,2,1), length 430
0.00451302528381
0.00053596496582
0.00142502784729
0.00343203544617
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,2,3), length 1730
0.0259549617767
0.00232696533203
0.00387692451477
0.0187470912933
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,2,5), length 7026
0.112207174301
0.0119769573212
0.0211799144745
0.0547370910645
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,4,1), length 1684
0.00609397888184
0.00121903419495
0.00452899932861
0.00959086418152
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,4,3), length 26828
0.19367814064
0.0293428897858
0.0688338279724
0.140627145767
()
Comparing deepcopy/marshal/ujson/json using random dataset(4,4,5), length 433484
3.54843020439
0.590909004211
1.09412097931
2.72070598602

1
谢谢。marshal是本主题中提到的所有替代方案中最快的,比cPickle稍微快一点。 - Marius
值得注意的是,marshal不支持循环引用自身的对象(我相信pickle可以),但json序列化也不行。 - Will S

1
你可以为对象提供自己的复制功能,这样就不需要深度复制。深度复制会检查每个对象以确定需要复制的内容,这是一个昂贵的操作。

我确实需要一份完全的副本。被转换的位也是最多的,因此导致deepcopy()要做更多的工作。 - Electro
@Electro 我认为他的意思是编写自己的深拷贝方法,可以复制所有内容,但不包括反射/内省部分。 - tobias_k
我阅读理解失败了。哎呀。嗯,但是反思真的是deepcopy()中最昂贵的操作吗? - Electro
2
是的,deepcopy() 必须确保不会出现引用循环等问题... 还涉及到很多繁琐的工作。请参见这里这里这里 - Bort

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接