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


当前回答

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

我用Python 3.8测试了它:

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

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

其他回答

我想发布一些不同于其他答案的内容。尽管这很可能不是最容易理解或最快的选项,但它提供了深度复制工作方式的一些内部视图,同时也是深度复制的另一种选择。我的函数是否有bug其实并不重要,因为这是为了展示一种复制问题答案之类的对象的方法,同时也是为了解释deepcopy的核心工作原理。

任何深度复制功能的核心都是创建浅层复制的方法。怎样易于理解的任何深度复制函数都只复制不可变对象的容器。当您深度复制嵌套列表时,您只复制外部列表,而不是列表内部的可变对象。您只是在复制容器。这同样适用于课堂。当您深度复制一个类时,您将深度复制它的所有可变属性。那么,如何?为什么你只需要复制容器,比如列表、字典、元组、迭代、类和类实例?

这很简单。可变对象不能真正复制。它永远无法更改,因此它只是一个值。这意味着您永远不必复制字符串、数字、布尔值或其中任何一个。但如何复制容器?易于理解的您只需要使用所有值初始化一个新容器。深度复制依赖于递归。它复制所有容器,甚至是其中有容器的容器,直到没有容器被留下。容器是一个不可变的对象。

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

def deepcopy(x):
  immutables = (str, int, bool, float)
  mutables = (list, dict, tuple)
  if isinstance(x, immutables):
    return x
  elif 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自己的内置deepcopy就是基于这个例子。唯一的区别是它支持其他类型,并且通过将属性复制到新的重复类中来支持用户类,并且还通过引用已经使用备忘录列表或字典看到的对象来阻止无限递归。这就是制作深度副本的真正原因。从其核心来看,制作深度副本只是制作浅层副本。我希望这个答案能为这个问题增添一些东西。

示例

假设您有以下列表:[1,2,3]。不可变的数字不能重复,但另一层可以。您可以使用列表理解复制它:[1,2,3]中的x代表x]

现在,假设您有一个列表:[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 x
  else:
    return [deepcopy_list(y) for y in x]

现在,您有了一个函数,它可以使用递归将str、bools、floast、int甚至列表的任何列表深度复制到无限多个层。这就是深度复制。

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

new_list = my_list[:]

new_list=我的列表

试着理解这一点。假设my_list位于堆内存中的位置X,即my_list指向X。现在,通过指定new_list=my_list,可以让new_list指向X。这就是所谓的浅拷贝。

现在,如果指定new_list=my_list[:],则只需将my_list的每个对象复制到new_list。这就是所谓的深度复制。

您可以通过以下其他方式完成此操作:

new_list=列表(old_list)导入副本new_list=复制.depcopy(old_list)

要使用的方法取决于要复制的列表的内容。如果列表中包含嵌套的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}
]}]

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

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

您可以使用“splat operator”:*执行列表解包,这也将复制列表的元素。

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)