Вопрос-Ответ

How do I clone a list so that it doesn't change unexpectedly after assignment?

Как мне клонировать список, чтобы он неожиданно не изменился после назначения?

При использовании new_list = my_list любые модификации new_list изменяются my_list каждый раз. Почему это так, и как я могу клонировать или копировать список, чтобы предотвратить это?

Переведено автоматически
Ответ 1

new_list = my_list на самом деле второй список не создается. Назначение просто копирует ссылку на список, а не сам список, поэтому оба new_list и my_list ссылаются на один и тот же список после назначения.

Чтобы фактически скопировать список, у вас есть несколько вариантов:


  • Вы можете использовать встроенный list.copy() метод (доступен начиная с Python 3.3):


    new_list = old_list.copy()


  • Вы можете нарезать его:


    new_list = old_list[:]

    Мнение Алекса Мартелли (по крайней мере, еще в 2007 году) по этому поводу таково, что это странный синтаксис и нет смысла его когда-либо использовать. ;) (По его мнению, следующий вариант более удобочитаем).



  • Вы можете использовать встроенный list() конструктор:


    new_list = list(old_list)


  • Вы можете использовать generic copy.copy():


    import copy
    new_list = copy.copy(old_list)

    Это немного медленнее, чем list() потому что сначала нужно определить тип данных old_list.



  • Если вам также нужно скопировать элементы списка, используйте generic copy.deepcopy():


    import copy
    new_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 instance
a.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)]
Ответ 2

Феликс уже предоставил отличный ответ, но я подумал, что мне стоит провести сравнение скорости различных методов:


  1. 10.59 сек (105.9 мкс / itn) - copy.deepcopy(old_list)

  2. 10.16 сек (101.6 мкс / itn) - чистый Python Copy() метод копирования классов с помощью deepcopy

  3. 1.488 сек (14.88 мкс / itn) - чистый Python Copy() метод, не копирующий классы (только dicts / списки / кортежи)

  4. 0,325 секунды (3,25 мкс / itn) - for item in old_list: new_list.append(item)

  5. 0.217 сек (2.17 мкс / itn) - [i for i in old_list] (для понимания списка)

  6. 0.186 сек (1.86 мкс / itn) - copy.copy(old_list)

  7. 0,075 сек (0,75 мкс / itn) - list(old_list)

  8. 0,053 сек (0,53 мкс / itn) - new_list = []; new_list.extend(old_list)

  9. 0,039 сек (0,39 мкс / 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 copying
is_tuple = True
obj = list(obj)
else:
# Otherwise just do a quick slice copy
obj = obj[:]
is_tuple = False

# Copy each item recursively
for x in xrange(len(obj)):
if type(obj[x]) in dignore:
continue
obj[x] = Copy(obj[x], use_deepcopy)

if is_tuple:
# Convert back into a tuple again
obj = 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:
continue
obj[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 copy
from time import time

num_times = 100000
L = [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
Ответ 3

Мне сказали, что Python 3.3+ добавляет list.copy() метод, который должен быть таким же быстрым, как нарезка:

newlist = old_list.copy()
Ответ 4

Какие есть варианты клонирования или копирования списка в 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[:]

Вы также можете выполнить то же самое, передав список через конструктор 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 затем изменяет 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, используйте deepcopy в copy модуле:

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

  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
2024-01-31 09:08 python list