如何克隆列表,使其不会在分配后意外更改?

使用new_list = my_list时,对new_list的任何修改每次都会更改my_list。为什么会这样,我如何克隆或复制列表以防止它?

2069150 次浏览

使用thing[:]

>>> a = [1,2]>>> b = a[:]>>> a += [3]>>> a[1, 2, 3]>>> b[1, 2]>>>

Python的习惯用法是newList = oldList[:]

new_list = my_list实际上并没有创建第二个列表。赋值只是复制对列表的引用,而不是实际列表,因此new_listmy_list在赋值后都引用了同一个列表。

要实际复制列表,您有几个选项:

  • 您可以使用内置#0方法(自Python 3.3起可用):

    new_list = old_list.copy()
  • 您可以将其切片:

    new_list = old_list[:]

    Alex Martelli对此的看法(至少早在2007)是,那这是一种奇怪的语法,使用它从来没有意义.;)(在他看来,下一个更具可读性)。

  • 您可以使用内置的#0构造函数:

    new_list = list(old_list)
  • 您可以使用泛型#0

    import copynew_list = copy.copy(old_list)

    这比list()慢一点,因为它必须先找出old_list的数据类型。

  • 如果您还需要复制列表的元素,请使用泛型#0

    import copynew_list = copy.deepcopy(old_list)

    显然是最慢和最需要内存的方法,但有时是不可避免的。这是递归操作的;它将处理任意数量的嵌套列表(或其他容器)级别。

示例:

import copy
class Foo(object):def __init__(self, val):self.val = val
def __repr__(self):return f'Foo({self.val!r})'
foo = Foo(1)
a = ['foo', foo]b = a.copy()c = a[:]d = list(a)e = copy.copy(a)f = copy.deepcopy(a)
# edit orignal list and instancea.append('baz')foo.val = 5
print(f'original: {a}\nlist.copy(): {b}\nslice: {c}\nlist(): {d}\ncopy: {e}\ndeepcopy: {f}')

结果:

original: ['foo', Foo(5), 'baz']list.copy(): ['foo', Foo(5)]slice: ['foo', Foo(5)]list(): ['foo', Foo(5)]copy: ['foo', Foo(5)]deepcopy: ['foo', Foo(1)]

菲利克斯已经提供了一个很好的答案,但我想我会对各种方法进行速度比较:

  1. 10.59秒(105.9µs/itn)-#0
  2. 10.16秒(101.6µs/itn)-纯PythonCopy()方法复制带有深度复制的类
  3. 1.488秒(14.88µs/itn)-纯PythonCopy()方法不复制类(只有dicts/list/tuple)
  4. 0.325秒(3.25µs/itn)-for item in old_list: new_list.append(item)
  5. 0.217秒(2.17µs/itn)-[i for i in old_list](a列表理解
  6. 0.186秒(1.86µs/itn)-#0
  7. 0.075秒(0.75µs/itn)-list(old_list)
  8. 0.053秒(0.53µs/itn)-new_list = []; new_list.extend(old_list)
  9. 0.039秒(0.39µs/itn)-old_list[:]列表切片

所以最快的是列表切片。但要注意copy.copy()list[:]list(list),与copy.deepcopy()和python版本不同,不会复制列表中的任何列表、字典和类实例,所以如果原始列表发生变化,它们也会在复制的列表中发生变化,反之亦然。

(如果有人感兴趣或想提出任何问题,这是脚本:)

from copy import deepcopy
class old_class:def __init__(self):self.blah = 'blah'
class new_class(object):def __init__(self):self.blah = 'blah'
dignore = {str: None, unicode: None, int: None, type(None): None}
def Copy(obj, use_deepcopy=True):t = type(obj)
if t in (list, tuple):if t == tuple:# Convert to a list if a tuple to# allow assigning to when copyingis_tuple = Trueobj = list(obj)else:# Otherwise just do a quick slice copyobj = obj[:]is_tuple = False
# Copy each item recursivelyfor x in xrange(len(obj)):if type(obj[x]) in dignore:continueobj[x] = Copy(obj[x], use_deepcopy)
if is_tuple:# Convert back into a tuple againobj = tuple(obj)
elif t == dict:# Use the fast shallow dict copy() method and copy any# values which aren't immutable (like lists, dicts etc)obj = obj.copy()for k in obj:if type(obj[k]) in dignore:continueobj[k] = Copy(obj[k], use_deepcopy)
elif t in dignore:# Numeric or string/unicode?# It's immutable, so ignore it!pass
elif use_deepcopy:obj = deepcopy(obj)return obj
if __name__ == '__main__':import copyfrom time import time
num_times = 100000L = [None, 'blah', 1, 543.4532,['foo'], ('bar',), {'blah': 'blah'},old_class(), new_class()]
t = time()for i in xrange(num_times):Copy(L)print 'Custom Copy:', time()-t
t = time()for i in xrange(num_times):Copy(L, use_deepcopy=False)print 'Custom Copy Only Copying Lists/Tuples/Dicts (no classes):', time()-t
t = time()for i in xrange(num_times):copy.copy(L)print 'copy.copy:', time()-t
t = time()for i in xrange(num_times):copy.deepcopy(L)print 'copy.deepcopy:', time()-t
t = time()for i in xrange(num_times):L[:]print 'list slicing [:]:', time()-t
t = time()for i in xrange(num_times):list(L)print 'list(L):', time()-t
t = time()for i in xrange(num_times):[i for i in L]print 'list expression(L):', time()-t
t = time()for i in xrange(num_times):a = []a.extend(L)print 'list extend:', time()-t
t = time()for i in xrange(num_times):a = []for y in L:a.append(y)print 'list append:', time()-t
t = time()for i in xrange(num_times):a = []a.extend(i for i in L)print 'generator expression extend:', time()-t

我已经被告知了Python 3.3+添加#0方法,它应该和切片一样快:

newlist = old_list.copy()

在Python中克隆或复制列表有哪些选项?

在Python 3中,可以使用以下方式制作浅拷贝:

a_copy = a_list.copy()

在Python 2和3中,您可以获得带有原始完整切片的浅副本:

a_copy = a_list[:]

补充说明

复制列表有两种语义方法。浅副本创建相同对象的新列表,深副本创建包含新等效对象的新列表。

浅列表复制

浅拷贝仅复制列表本身,列表本身是对列表中对象的引用的容器。如果包含的对象本身是可变的,并且一个被更改,则更改将反映在两个列表中。

在Python 2和3中有不同的方法可以做到这一点。Python 2的方法也适用于Python 3。

python2

在Python 2中,制作列表浅拷贝的惯用方法是使用原始的完整切片:

a_copy = a_list[:]

您也可以通过通过列表构造函数传递列表来完成同样的事情,

a_copy = list(a_list)

但是使用构造函数效率较低:

>>> timeit>>> l = range(20)>>> min(timeit.repeat(lambda: l[:]))0.30504298210144043>>> min(timeit.repeat(lambda: list(l)))0.40698814392089844

python3

在Python 3中,列表获取list.copy方法:

a_copy = a_list.copy()

在Python 3.5中:

>>> import timeit>>> l = list(range(20))>>> min(timeit.repeat(lambda: l[:]))0.38448613602668047>>> min(timeit.repeat(lambda: list(l)))0.6309100328944623>>> min(timeit.repeat(lambda: l.copy()))0.38122922903858125

做另一个指针没有做一个副本

使用new_list=my_list然后修改new_list每次my_list变化。

my_list只是一个指向内存中实际列表的名称。当你说new_list = my_list时,你不是在复制,你只是在内存中添加另一个指向原始列表的名称。当我们复制列表时,我们可能会遇到类似的问题。

>>> l = [[], [], []]>>> l_copy = l[:]>>> l_copy[[], [], []]>>> l_copy[0].append('foo')>>> l_copy[['foo'], [], []]>>> l[['foo'], [], []]

列表只是指向内容的指针数组,所以浅拷贝只是复制指针,所以你有两个不同的列表,但它们有相同的内容。要复制内容,你需要一个深拷贝。

深度拷贝

做一个列表的深度副本,在Python 2或3中,在#1模块中使用#0

import copya_deep_copy = copy.deepcopy(a_list)

为了演示这如何使我们能够创建新的子列表:

>>> import copy>>> l[['foo'], [], []]>>> l_deep_copy = copy.deepcopy(l)>>> l_deep_copy[0].pop()'foo'>>> l_deep_copy[[], [], []]>>> l[['foo'], [], []]

因此,我们看到深度复制列表与原始列表完全不同。您可以滚动自己的函数-但不要。您可能会创建使用标准库的深度复制函数不会产生的错误。

不要使用eval

你可能会看到这被用作深度复制的一种方式,但不要这样做:

problematic_deep_copy = eval(repr(a_list))
  1. 这是危险的,特别是如果你从一个你不信任的来源评估一些东西。
  2. 这是不可靠的,如果你复制的子元素没有一个表示,可以被评估来再现一个等效的元素。
  3. 它的性能也较差。

在64位Python 2.7中:

>>> import timeit>>> import copy>>> l = range(10)>>> min(timeit.repeat(lambda: copy.deepcopy(l)))27.55826997756958>>> min(timeit.repeat(lambda: eval(repr(l))))29.04534101486206

在64位Python 3.5上:

>>> import timeit>>> import copy>>> l = list(range(10))>>> min(timeit.repeat(lambda: copy.deepcopy(l)))16.84255409205798>>> min(timeit.repeat(lambda: eval(repr(l))))34.813894678023644

已经有很多答案告诉你如何制作一个合适的副本,但没有一个说为什么你的原始“副本”失败了。

Python不将值存储在变量中;它将名称绑定到对象。您的原始赋值采用my_list引用的对象并将其绑定到new_list。无论您使用哪个名称,仍然只有一个列表,因此将其称为my_list时所做的更改将在将其称为new_list时保持不变。这个问题的每个其他答案都为您提供了创建绑定到new_list的新对象的不同方法。

列表的每个元素都像一个名称,因为每个元素都非排他性地绑定到一个对象。浅拷贝创建一个新列表,其元素绑定到与以前相同的对象。

new_list = list(my_list)  # or my_list[:], but I prefer this syntax# is simply a shorter way of:new_list = [element for element in my_list]

要进一步复制列表,请复制列表引用的每个对象,并将这些元素副本绑定到新列表。

import copy# each element must have __copy__ defined for this...new_list = [copy.copy(element) for element in my_list]

这还不是深度复制,因为列表的每个元素都可能引用其他对象,就像列表绑定到其元素一样。要递归复制列表中的每个元素,然后每个元素引用的每个其他对象,依此类推:执行深度复制。

import copy# each element must have __deepcopy__ defined for this...new_list = copy.deepcopy(my_list)

有关复制中的角情况的更多信息,请参阅留档

所有其他贡献者都给出了伟大的答案,当你有一个单维(水平)列表时,这是有效的,然而,在迄今为止提到的方法中,只有copy.deepcopy()可以克隆/复制列表,而不是让它指向嵌套的list对象,当你使用多维嵌套列表(列表的列表)时。虽然FelixKling在他的回答中提到了它,但这个问题还有更多,可能是一个使用内置插件的解决方法,可能会证明是deepcopy的更快替代方案。

虽然new_list = old_list[:]copy.copy(old_list)'和Py3kold_list.copy()适用于单级列表,但它们恢复指向嵌套在old_listnew_list中的list对象,并且对list对象之一的更改在另一个中永久化。

编辑:新信息曝光

正如亚伦·霍尔PM 2Ring使用#0不仅是一个坏主意,而且比#1慢得多。所指出的那样

这意味着对于多维列表,唯一的选择是copy.deepcopy()。话虽如此,它真的不是一个选项,因为当你尝试在中等大小的多维数组上使用它时,性能会下降。我尝试使用42×42数组timeit,对于生物信息学应用来说,这不是闻所未闻的,甚至不是那么大,我放弃了等待回复,刚刚开始在这篇文章中输入我的编辑。

似乎唯一真正的选择是初始化多个列表并独立处理它们。如果有人对如何处理多维列表复制有任何其他建议,我们将不胜感激。

正如其他人所说,使用copy模块和copy.deepcopy用于多维列表存在<强>意义重大性能问题。

Python 3.6计时

以下是使用Python 3.6.8的计时结果。请记住,这些时间是相对的,而不是绝对的。

我坚持只做浅拷贝,并添加了一些在Python 2中不可能的新方法,例如list.copy()(Python 3切片等效)和列表拆包的两种形式(*new_list, = listnew_list = [*list]):

METHOD                TIME TAKENb = [*a]               2.75180600000021b = a * 1              3.50215399999990b = a[:]               3.78278899999986  # Python 2 winner (see above)b = a.copy()           4.20556500000020  # Python 3 "slice equivalent" (see above)b = []; b.extend(a)    4.68069800000012b = a[0:len(a)]        6.84498999999959*b, = a                7.54031799999984b = list(a)            7.75815899999997b = [i for i in a]    18.4886440000000b = copy.copy(a)      18.8254879999999b = []for item in a:b.append(item)      35.4729199999997

我们可以看到Python 2的获胜者仍然做得很好,但并没有远远超过Python 3list.copy(),特别是考虑到后者的卓越易读性。

黑马是拆包和重新包装方法(b = [*a]),它比原始切片快约25%,比其他拆包方法(*b, = a)快两倍多。

b = a * 1也做得出奇的好。

请注意,这些方法不会为列表以外的任何输入输出等效的结果。它们都适用于可切片对象,少数适用于任何可迭代对象,但只有copy.copy()适用于更通用的Python对象。


以下是相关方的测试代码(模板从这里):

import timeit
COUNT = 50000000print("Array duplicating. Tests run", COUNT, "times")setup = 'a = [0,1,2,3,4,5,6,7,8,9]; import copy'
print("b = list(a)\t\t", timeit.timeit(stmt='b = list(a)', setup=setup, number=COUNT))print("b = copy.copy(a)\t", timeit.timeit(stmt='b = copy.copy(a)', setup=setup, number=COUNT))print("b = a.copy()\t\t", timeit.timeit(stmt='b = a.copy()', setup=setup, number=COUNT))print("b = a[:]\t\t", timeit.timeit(stmt='b = a[:]', setup=setup, number=COUNT))print("b = a[0:len(a)]\t\t", timeit.timeit(stmt='b = a[0:len(a)]', setup=setup, number=COUNT))print("*b, = a\t\t\t", timeit.timeit(stmt='*b, = a', setup=setup, number=COUNT))print("b = []; b.extend(a)\t", timeit.timeit(stmt='b = []; b.extend(a)', setup=setup, number=COUNT))print("b = []; for item in a: b.append(item)\t", timeit.timeit(stmt='b = []\nfor item in a:  b.append(item)', setup=setup, number=COUNT))print("b = [i for i in a]\t", timeit.timeit(stmt='b = [i for i in a]', setup=setup, number=COUNT))print("b = [*a]\t\t", timeit.timeit(stmt='b = [*a]', setup=setup, number=COUNT))print("b = a * 1\t\t", timeit.timeit(stmt='b = a * 1', setup=setup, number=COUNT))
new_list = my_list[:]

new_list = my_list

试着理解这一点。假设my_list在位置X的堆内存中,即my_list指向X。现在通过分配new_list = my_list,您让new_list指向X。这被称为浅拷贝

现在,如果您分配new_list = my_list[:],您只需将my_list的每个对象复制到new_list。这被称为深度复制

你可以做到这一点的其他方法是:

  • new_list = list(old_list)
  • import copynew_list = copy.deepcopy(old_list)

在已经给出的答案中缺少一个独立于python版本的非常简单的方法,您可以在大多数时候使用它(至少我这样做):

new_list = my_list * 1       # Solution 1 when you are not using nested lists

但是,如果my_list包含其他容器(例如,嵌套列表),您必须使用深度复制作为复制库中上面答案中建议的其他容器。例如:

import copynew_list = copy.deepcopy(my_list)   # Solution 2 when you are using nested lists

.奖金:如果您不想复制元素,请使用(又名浅拷贝):

new_list = my_list[:]

让我们了解解决方案#1和解决方案#2之间的区别

>>> a = range(5)>>> b = a*1>>> a,b([0, 1, 2, 3, 4], [0, 1, 2, 3, 4])>>> a[2] = 55>>> a,b([0, 1, 55, 3, 4], [0, 1, 2, 3, 4])

如您所见,当我们不使用嵌套列表时,解决方案#1工作得很好。让我们看看当我们将解决方案#1应用于嵌套列表时会发生什么。

>>> from copy import deepcopy>>> a = [range(i,i+4) for i in range(3)]>>> a[[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]]>>> b = a*1>>> c = deepcopy(a)>>> for i in (a, b, c): print i[[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]][[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]][[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]]>>> a[2].append('99')>>> for i in (a, b, c): print i[[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5, 99]][[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5, 99]]   # Solution #1 didn't work in nested list[[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5]]       # Solution #2 - DeepCopy worked in nested list

让我们从头开始,探讨这个问题。

假设你有两个列表:

list_1 = ['01', '98']list_2 = [['01', '98']]

我们必须复制两个列表,现在从第一个列表开始:

所以首先让我们尝试将变量copy设置为我们的原始列表list_1

copy = list_1

现在,如果你认为复制复制了list_1,那么你就错了。id函数可以告诉我们两个变量是否可以指向同一个对象。让我们试试这个:

print(id(copy))print(id(list_1))

输出是:

43294853204329485320

两个变量是完全相同的参数。你感到惊讶吗?

众所周知,Python不会在变量中存储任何东西,变量只是引用对象,对象存储值。这里的对象是list,但我们通过两个不同的变量名创建了对同一对象的两次引用。这意味着两个变量都指向同一个对象,只是名称不同。

当你做copy = list_1时,它实际上是在做:

在此处输入图片描述

在图像list_1复制中,有两个变量名,但两个变量的对象是相同的,即list

因此,如果您尝试修改复制列表,那么它也会修改原始列表,因为列表只有一个,无论您从复制列表还是从原始列表进行修改,您都将修改该列表:

copy[0] = "modify"
print(copy)print(list_1)

输出:

['modify', '98']['modify', '98']

所以它修改了原始列表:

现在让我们转向复制列表的Pythonic方法。

copy_1 = list_1[:]

此方法修复了我们遇到的第一个问题:

print(id(copy_1))print(id(list_1))
43387921364338791432

所以我们可以看到我们的两个列表都有不同的id,这意味着两个变量都指向不同的对象。所以这里实际发生的是:

在此处输入图片描述

现在让我们尝试修改列表,看看我们是否仍然面临前面的问题:

copy_1[0] = "modify"
print(list_1)print(copy_1)

输出是:

['01', '98']['modify', '98']

如您所见,它只修改了复制的列表。这意味着它起作用了。

你认为我们完成了吗?不。让我们尝试复制我们的嵌套列表。

copy_2 = list_2[:]

list_2应该引用另一个对象,它是list_2的副本。让我们检查一下:

print(id((list_2)), id(copy_2))

我们得到输出:

4330403592 4330403528

现在我们可以假设两个列表都指向不同的对象,所以现在让我们尝试修改它,让我们看看它给出了我们想要的:

copy_2[0][1] = "modify"
print(list_2, copy_2)

这给了我们输出:

[['01', 'modify']] [['01', 'modify']]

这可能看起来有点令人困惑,因为我们之前使用的相同方法有效。让我们试着理解这一点。

当你这样做:

copy_2 = list_2[:]

您只复制外部列表,而不是内部列表。我们可以再次使用id函数来检查这一点。

print(id(copy_2[0]))print(id(list_2[0]))

输出是:

43294858324329485832

当我们做copy_2 = list_2[:]时,会发生这样的事情:

在此处输入图片描述

它创建列表的副本,但只创建外部列表副本,而不是嵌套列表副本。嵌套列表对于两个变量都是相同的,因此如果您尝试修改嵌套列表,那么它也会修改原始列表,因为嵌套列表对象对于两个列表都是相同的。

解决方案是什么?解决方案是deepcopy函数。

from copy import deepcopydeep = deepcopy(list_2)

让我们检查一下:

print(id((list_2)), id(deep))
4322146056 4322148040

两个外部列表都有不同的ID。让我们在内部嵌套列表上尝试一下。

print(id(deep[0]))print(id(list_2[0]))

输出是:

43221459924322145800

正如你所看到的,两个ID是不同的,这意味着我们可以假设两个嵌套列表现在指向不同的对象。

这意味着当你做deep = deepcopy(list_2)时实际发生了什么:

在此处输入图片描述

两个嵌套列表都指向不同的对象,它们现在有单独的嵌套列表副本。

现在让我们尝试修改嵌套列表,看看它是否解决了前面的问题:

deep[0][1] = "modify"print(list_2, deep)

它输出:

[['01', '98']] [['01', 'modify']]

如您所见,它没有修改原始嵌套列表,它只修改了复制的列表。

令我惊讶的是,这还没有被提及,所以为了完整起见…

您可以使用“spat运算符”:*执行列表解包,它也会复制列表中的元素。

old_list = [1, 2, 3]
new_list = [*old_list]
new_list.append(4)old_list == [1, 2, 3]new_list == [1, 2, 3, 4]

这种方法的明显缺点是它仅在Python 3.5+中可用。

虽然时机明智,但这似乎比其他常见方法表现更好。

x = [random.random() for _ in range(1000)]
%timeit a = list(x)%timeit a = x.copy()%timeit a = x[:]
%timeit a = [*x]
#: 2.47 µs ± 38.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)#: 2.47 µs ± 54.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)#: 2.39 µs ± 58.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
#: 2.22 µs ± 43.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

请注意,在某些情况下,如果您已经定义了自己的自定义类并且想要保留属性,那么您应该使用copy.copy()copy.deepcopy()而不是替代方案,例如在Python 3中:

import copy
class MyList(list):pass
lst = MyList([1,2,3])
lst.name = 'custom list'
d = {'original': lst,'slicecopy' : lst[:],'lstcopy' : lst.copy(),'copycopy': copy.copy(lst),'deepcopy': copy.deepcopy(lst)}

for k,v in d.items():print('lst: {}'.format(k), end=', ')try:name = v.nameexcept AttributeError:name = 'NA'print('name: {}'.format(name))

产出:

lst: original, name: custom listlst: slicecopy, name: NAlst: lstcopy, name: NAlst: copycopy, name: custom listlst: deepcopy, name: custom list

我想发布一些与其他答案有点不同的东西。尽管这很可能不是最容易理解或最快的选项,但它提供了一些关于深度复制如何工作的内部视图,并且是深度复制的另一个选择。如果我的函数有错误并不重要,因为这的重点是展示一种复制对象(如问题答案)的方法,但也以此作为一个点来解释深度复制的核心工作原理。

任何深度复制函数的核心都是进行浅复制。怎么做?很简单。任何深度复制函数都只复制不可变对象的容器。当你深度复制嵌套列表时,你只是复制了外部列表,而不是列表中的可变对象。你只是复制了容器。类也是如此。当你深度复制一个类时,你要深度复制它所有的可变属性。那么,怎么做呢?为什么你只需要复制容器,比如列表、dict、元组、iters、类和类实例?

很简单。一个可变对象不能真正被复制。它永远不能被改变,所以它只有一个值。这意味着你永远不必复制字符串、数字、布尔值或任何这些。但是如何复制容器呢?很简单。你只需用所有的值初始化一个新容器。深度复制依赖于递归。它复制所有的容器,甚至里面有容器的容器,直到没有容器剩下。容器是一个不可变的对象。

一旦你知道了这一点,在没有任何引用的情况下完全复制一个对象是非常容易的。这是一个用于深度复制基本数据类型的函数(不适用于自定义类,但你可以随时添加它)

def deepcopy(x):immutables = (str, int, bool, float)mutables = (list, dict, tuple)if isinstance(x, immutables):return xelif isinstance(x, mutables):if isinstance(x, tuple):return tuple(deepcopy(list(x)))elif isinstance(x, list):return [deepcopy(y) for y in x]elif isinstance(x, dict):values = [deepcopy(y) for y in list(x.values())]keys = list(x.keys())return dict(zip(keys, values))

Python自己的内置深度拷贝就是基于这个例子。唯一的区别是它支持其他类型,并且还支持通过将属性复制到一个新的复制类中来支持用户类,并且还通过引用已经使用备忘录列表或字典看到的对象来阻止无限递归。这就是制作深度拷贝的真正原因。其核心是,制作深度拷贝只是制作浅拷贝。我希望这个答案能为这个问题增加一些东西。

示例

假设您有这个列表:[1, 2, 3]。不可变数字不能复制,但其他层可以。您可以使用列表推导来复制它:[x for x in [1, 2, 3]]

现在,假设你有这个列表:[[1, 2], [3, 4], [5, 6]]。这一次,你想做一个函数,它使用递归来深度复制列表的所有层。而不是之前的列表理解:

[x for x in _list]

它使用一个新的列表:

[deepcopy_list(x) for x in _list]

deepcopy_list看起来像这样:

def deepcopy_list(x):if isinstance(x, (str, bool, float, int)):return xelse:return [deepcopy_list(y) for y in x]

然后现在你有了一个函数,它可以使用递归将任何strs、bools、flast、int甚至列表的列表深度复制到无限多层。

TLDR:DeepCopy使用递归来复制对象,并且只返回与之前相同的不可变对象,因为不可变对象无法复制。但是,它会深度复制可变对象的最内层,直到到达对象的最外层可变层。

通过id和gc查看内存的一个稍微实用的角度。

>>> b = a = ['hell', 'word']>>> c = ['hell', 'word']
>>> id(a), id(b), id(c)(4424020872, 4424020872, 4423979272)|           |-----------
>>> id(a[0]), id(b[0]), id(c[0])(4424018328, 4424018328, 4424018328) # all referring to same 'hell'|           |           |-----------------------
>>> id(a[0][0]), id(b[0][0]), id(c[0][0])(4422785208, 4422785208, 4422785208) # all referring to same 'h'|           |           |-----------------------
>>> a[0] += 'o'>>> a,b,c(['hello', 'word'], ['hello', 'word'], ['hell', 'word'])  # b changed too>>> id(a[0]), id(b[0]), id(c[0])(4424018384, 4424018384, 4424018328) # augmented assignment changed a[0],b[0]|           |-----------
>>> b = a = ['hell', 'word']>>> id(a[0]), id(b[0]), id(c[0])(4424018328, 4424018328, 4424018328) # the same hell|           |           |-----------------------
>>> import gc>>> gc.get_referrers(a[0])[['hell', 'word'], ['hell', 'word']]  # one copy belong to a,b, the another for c>>> gc.get_referrers(('hell'))[['hell', 'word'], ['hell', 'word'], ('hell', None)] # ('hello', None)

记住,在Python中,当你这样做时:

    list1 = ['apples','bananas','pineapples']list2 = list1

List2不是存储实际列表,而是对list1的引用。所以当你对list1做任何事情时,list2也会改变。使用复制模块(不是默认的,在pip上下载)制作列表的原始副本(copy.copy()用于简单列表,copy.deepcopy()用于嵌套列表)。这使得一个副本不会随着第一个列表而改变。

深度复制选项是唯一适合我的方法:

from copy import deepcopy
a = [   [ list(range(1, 3)) for i in range(3) ]   ]b = deepcopy(a)b[0][1]=[3]print('Deep:')print(a)print(b)print('-----------------------------')a = [   [ list(range(1, 3)) for i in range(3) ]   ]b = a*1b[0][1]=[3]print('*1:')print(a)print(b)print('-----------------------------')a = [   [ list(range(1, 3)) for i in range(3) ] ]b = a[:]b[0][1]=[3]print('Vector copy:')print(a)print(b)print('-----------------------------')a = [   [ list(range(1, 3)) for i in range(3) ]  ]b = list(a)b[0][1]=[3]print('List copy:')print(a)print(b)print('-----------------------------')a = [   [ list(range(1, 3)) for i in range(3) ]  ]b = a.copy()b[0][1]=[3]print('.copy():')print(a)print(b)print('-----------------------------')a = [   [ list(range(1, 3)) for i in range(3) ]  ]b = ab[0][1]=[3]print('Shallow:')print(a)print(b)print('-----------------------------')

导致输出:

Deep:[[[1, 2], [1, 2], [1, 2]]][[[1, 2], [3], [1, 2]]]-----------------------------*1:[[[1, 2], [3], [1, 2]]][[[1, 2], [3], [1, 2]]]-----------------------------Vector copy:[[[1, 2], [3], [1, 2]]][[[1, 2], [3], [1, 2]]]-----------------------------List copy:[[[1, 2], [3], [1, 2]]][[[1, 2], [3], [1, 2]]]-----------------------------.copy():[[[1, 2], [3], [1, 2]]][[[1, 2], [3], [1, 2]]]-----------------------------Shallow:[[[1, 2], [3], [1, 2]]][[[1, 2], [3], [1, 2]]]-----------------------------

这是因为,行new_list = my_list为变量my_list分配了一个新的引用,即new_list这类似于下面给出的C代码,

int my_list[] = [1,2,3,4];int *new_list;new_list = my_list;

您应该使用复制模块通过以下方式创建一个新列表

import copynew_list = copy.deepcopy(my_list)

还有另一种方法可以复制到现在还没有列出的列表:添加一个空列表:l2 = l + []

我用Python 3.8测试了它:

l = [1,2,3]l2 = l + []print(l,l2)l[0] = 'a'print(l,l2)

这不是最好的答案,但它奏效了。

要使用的方法取决于要复制的列表的内容。如果列表包含嵌套dicts,那么深度复制是唯一有效的方法,否则答案中列出的大多数方法(切片、循环[for]、复制、扩展、组合或解包)将在类似的时间内工作和执行(循环和深度复制除外,这是最差的)。

脚本

from random import randintfrom time import timeimport copy
item_count = 100000
def copy_type(l1: list, l2: list):if l1 == l2:return 'shallow'return 'deep'
def run_time(start, end):run = end - startreturn int(run * 1000000)
def list_combine(data):l1 = [data for i in range(item_count)]start = time()l2 = [] + l1end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'combine', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
def list_extend(data):l1 = [data for i in range(item_count)]start = time()l2 = []l2.extend(l1)end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'extend', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
def list_unpack(data):l1 = [data for i in range(item_count)]start = time()l2 = [*l1]end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'unpack', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
def list_deepcopy(data):l1 = [data for i in range(item_count)]start = time()l2 = copy.deepcopy(l1)end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'deepcopy', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
def list_copy(data):l1 = [data for i in range(item_count)]start = time()l2 = list.copy(l1)end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'copy', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
def list_slice(data):l1 = [data for i in range(item_count)]start = time()l2 = l1[:]end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'slice', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
def list_loop(data):l1 = [data for i in range(item_count)]start = time()l2 = []for i in range(len(l1)):l2.append(l1[i])end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'loop', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
def list_list(data):l1 = [data for i in range(item_count)]start = time()l2 = list(l1)end = time()if type(data) == dict:l2[0]['test'].append(1)elif type(data) == list:l2.append(1)return {'method': 'list()', 'copy_type': copy_type(l1, l2),'time_µs': run_time(start, end)}
if __name__ == '__main__':list_type = [{'list[dict]': {'test': [1, 1]}},{'list[list]': [1, 1]}]store = []for data in list_type:key = list(data.keys())[0]store.append({key: [list_unpack(data[key]), list_extend(data[key]),list_combine(data[key]), list_deepcopy(data[key]),list_copy(data[key]), list_slice(data[key]),list_loop(data[key])]})print(store)

搜索结果

[{"list[dict]": [{"method": "unpack", "copy_type": "shallow", "time_µs": 56149},{"method": "extend", "copy_type": "shallow", "time_µs": 52991},{"method": "combine", "copy_type": "shallow", "time_µs": 53726},{"method": "deepcopy", "copy_type": "deep", "time_µs": 2702616},{"method": "copy", "copy_type": "shallow", "time_µs": 52204},{"method": "slice", "copy_type": "shallow", "time_µs": 52223},{"method": "loop", "copy_type": "shallow", "time_µs": 836928}]},{"list[list]": [{"method": "unpack", "copy_type": "deep", "time_µs": 52313},{"method": "extend", "copy_type": "deep", "time_µs": 52550},{"method": "combine", "copy_type": "deep", "time_µs": 53203},{"method": "deepcopy", "copy_type": "deep", "time_µs": 2608560},{"method": "copy", "copy_type": "deep", "time_µs": 53210},{"method": "slice", "copy_type": "deep", "time_µs": 52937},{"method": "loop", "copy_type": "deep", "time_µs": 834774}]}]

框架挑战:你真的需要为你的应用程序复制吗?

我经常看到试图以某种迭代方式修改列表副本的代码。为了构造一个简单的例子,假设我们有非工作(因为x不应该被修改)代码,如:

x = [8, 6, 7, 5, 3, 0, 9]y = xfor index, element in enumerate(y):y[index] = element * 2# Expected result:# x = [8, 6, 7, 5, 3, 0, 9] <-- this is where the code is wrong.# y = [16, 12, 14, 10, 6, 0, 18]

自然,人们会问如何使y成为x的副本,而不是同一个列表的名称,这样for循环就会做正确的事情。

但这是错误的方法。从功能上讲,我们真的想要做的是让新名单成为基于的原始。

我们不需要先复制来做到这一点,我们通常不应该这样做。

当我们需要将逻辑应用于每个元素时

这样做的自然工具是列表理解。通过这种方式,我们编写逻辑,告诉我们所需结果中的元素与原始元素的关系。它简单、优雅和富有表现力;我们避免了在for循环中修改y副本的变通方法(从分配给迭代变量不会影响列表-出于同样的原因,我们首先想要的副本!开始)。

对于上面的示例,它看起来像:

x = [8, 6, 7, 5, 3, 0, 9]y = [element * 2 for element in x]

列表推导非常强大;我们还可以用它们来通过带有if子句的规则过滤掉元素,我们可以链接forif子句(它的工作方式类似于相应的命令式代码,具有相同的子句以相同的顺序;只有最终将在结果列表中结束的值被移动到前面而不是“最里面”的部分)。如果计划是在修改副本以避免问题时迭代原始内容,通常有一个更愉快的方法来实现这一点过滤列表推导。

当我们需要按位置拒绝或插入特定元素时

假设我们有这样的东西

x = [8, 6, 7, 5, 3, 0, 9]y = xdel y[2:-2] # oops, x was changed inappropriately

与其先将y作为单独的副本以删除我们不想要的部分,我们可以通过构建一个我们想要的部分的列表。因此:

x = [8, 6, 7, 5, 3, 0, 9]y = x[:2] + x[-2:]

通过切片处理插入、替换等留作练习。只需推理出你希望结果包含哪些子序列。这种情况的一个特例是做一个颠倒的拷贝-假设我们需要一个新列表(而不仅仅是反向迭代),我们可以直接通过切片创建它,而不是克隆然后使用.reverse


这些方法——比如列表理解——还有一个优点,即它们创建了所需的结果作为一种表达,而不是通过在程序上就地修改现有对象(和返回#0)。这对于以“流畅”的风格编写代码更方便。

每个复制模式的简短解释:

浅拷贝构造一个新的复合对象,然后(尽可能地)将引用插入到原始对象中-创建一个浅拷贝:

new_list = my_list

深度复制构造一个新的复合对象,然后递归地将原始对象中找到的对象的副本插入其中-创建一个深度副本:

new_list = list(my_list)

list()适用于简单列表的深度副本,例如:

my_list = ["A","B","C"]

但是,对于复杂的列表,如…

my_complex_list = [{'A' : 500, 'B' : 501},{'C' : 502}]

…使用deepcopy()

import copynew_complex_list = copy.deepcopy(my_complex_list)