使用new_list=my_list时,对new_list的任何修改都会每次更改my_list。为什么会出现这种情况,以及如何克隆或复制列表以防止出现这种情况?


当前回答

有人告诉我Python 3.3+添加了list.copy()方法,它应该和切片一样快:

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。

Python 2

在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

Python 3

在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,然后在每次my_list更改时修改new_list。这是为什么?

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

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

列表只是指向内容的指针数组,因此浅层副本只是复制指针,因此您有两个不同的列表,但它们具有相同的内容。要复制内容,您需要一个深度副本。

深度副本

要制作列表的深度副本,在Python 2或3中,请在复制模块中使用deepcopy:

import copy
a_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'], [], []]

所以我们看到,深度复制的列表与原始列表完全不同。你可以滚动自己的函数,但不要。通过使用标准库的deepcopy功能,您很可能会创建一些错误。

不使用eval

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

problematic_deep_copy = eval(repr(a_list))

这是很危险的,特别是当你从一个你不信任的来源评估某件事情时。如果要复制的子元素没有一个可以求值以重现等效元素的表示,那么它就不可靠。它的性能也较差。

在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

框架挑战:对于您的应用程序,您实际上需要复制吗?

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

x = [8, 6, 7, 5, 3, 0, 9]
y = x
for 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子句的规则过滤掉元素,并且我们可以链接for和if子句(它的工作方式与相应的命令式代码类似,相同的子句的顺序相同;只有最终将在结果列表中结束的值才会移到前面,而不是在“最里面”部分)。如果计划是在修改副本以避免问题的同时迭代原始文件,那么通常有一种更令人愉快的方法来实现这一点,即理解过滤列表。

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

假设我们有这样的东西

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

我们可以通过将我们不需要的部分放在一起来建立一个列表,而不是先创建一个单独的副本来删除我们不想要的部分。因此:

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

通过切片处理插入、替换等操作是一项练习。只需说明您希望结果包含哪些子序列。这种情况的一个特殊情况是制作一个反向副本-假设我们需要一个新列表(而不仅仅是反向迭代),我们可以通过切片直接创建它,而不是克隆然后使用.reverse。


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

所有其他贡献者都给出了很好的答案,当你有一个单一维度(水平化)列表时,这些方法是有效的,但是在目前提到的方法中,只有copy.deepcopy()可以克隆/复制列表,而当你使用多维嵌套列表(列表列表)时,它不会指向嵌套列表对象。虽然菲利克斯·克林在他的回答中提到了这一点,但这个问题还有一点问题,可能还有一个使用内置程序的解决方案,这可能会证明是深度复制的更快替代方案。

虽然new_list=old_list[:],copy.copy(old_list)'和Py3k old_list.copy()适用于单层列表,但它们恢复为指向嵌套在old_list和new_list中的列表对象,对其中一个列表对象的更改将在另一个列表中永久化。

编辑:新信息曝光

正如Aaron Hall和PM 2Ring所指出的那样,使用eval()不仅是一个坏主意,而且比copy.deepcopy()慢得多。这意味着,对于多维列表,唯一的选项是copy.deepcopy()。尽管如此,当您尝试在中等大小的多维数组上使用它时,它确实不是一个选项,因为性能会下降。我尝试使用42x42阵列来计时,这是前所未闻的,甚至对于生物信息学应用程序来说也是如此之大,我放弃了等待响应,只是开始在这篇文章中输入我的编辑。似乎唯一真正的选择就是初始化多个列表并独立处理它们。如果有人对如何处理多维列表复制有任何其他建议,将不胜感激。

正如其他人所说的那样,在多维列表中使用copy模块和copy.devcopy存在严重的性能问题。

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

假设您有两个列表:

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

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

因此,首先让我们将变量副本设置为原始列表list_1:

copy = list_1

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

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

输出为:

4329485320
4329485320

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

所以我们知道,Python不会在变量中存储任何内容,变量只是引用对象,对象存储值。这里的对象是一个列表,但我们通过两个不同的变量名创建了对同一对象的两个引用。这意味着两个变量都指向同一个对象,只是名称不同。

当您执行copy=list_1时,它实际上正在执行以下操作:

在这里,图像list_1和copy是两个变量名,但两个变量的对象是相同的,即列表。

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

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))

4338792136
4338791432

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

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

copy_1[0] = "modify"

print(list_1)
print(copy_1)

输出为:

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

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

你认为我们结束了吗?不,让我们尝试复制嵌套列表。

copy_2 = list_2[:]

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

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]))

输出为:

4329485832
4329485832

当我们执行copy_2=list_2[:]时,会发生以下情况:

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

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

from copy import deepcopy
deep = deepcopy(list_2)

让我们检查一下:

print(id((list_2)), id(deep))

4322146056 4322148040

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

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

输出为:

4322145992
4322145800

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

这意味着当您执行deep=deepcopy(list_2)时,实际发生了什么:

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

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

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

它输出:

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

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

要使用的方法取决于要复制的列表的内容。如果列表中包含嵌套的dict,则deepcopy是唯一有效的方法,否则答案中列出的大多数方法(slice、loop[for]、copy、extend、combine或unpack)都将在类似的时间内工作和执行(loop和deepcopy除外,这两种方法执行得最差)。

剧本

from random import randint
from time import time
import 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 - start
  return int(run * 1000000)

def list_combine(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': '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}
]}]