Как клонировать или скопировать список? - PullRequest
2138 голосов
/ 10 апреля 2010

Какие есть варианты клонирования или копирования списка в Python?

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

Ответы [ 15 ]

2860 голосов
/ 10 апреля 2010

С 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)
    
  • Вы можете использовать универсальный 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 str(self.val)

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('original: %r\n list.copy(): %r\n slice: %r\n list(): %r\n copy: %r\n deepcopy: %r'
      % (a, b, c, d, e, f))

Результат:

original: ['foo', 5, 'baz']
list.copy(): ['foo', 5]
slice: ['foo', 5]
list(): ['foo', 5]
copy: ['foo', 5]
deepcopy: ['foo', 1]
529 голосов
/ 10 апреля 2010

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

  1. 10,59 с (105,9us / itn) - copy.deepcopy(old_list)
  2. 10,16 с (101,6us / itn) - чистый питон Copy() метод, копирующий классы с глубокой копией
  3. 1,488 сек (14,88us / itn) - чистый питон Copy() метод, не копирующий классы (только dicts / lists / tuples)
  4. 0,325 с (3,25 мкс / миль) - for item in old_list: new_list.append(item)
  5. 0,217 с (2,17us / itn) - [i for i in old_list] ( понимание списка )
  6. 0,186 с (1,86US / ITN) - copy.copy(old_list)
  7. 0,075 с (0,75us / itn) - list(old_list)
  8. 0,053 с (0,53us / itn) - new_list = []; new_list.extend(old_list)
  9. 0,039 с (0,39us / 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
132 голосов
/ 23 июля 2013

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

newlist = old_list.copy()

114 голосов
/ 25 октября 2014

Какие есть варианты клонирования или копирования списка в 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 затем изменяет 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
49 голосов
/ 23 ноября 2014

Уже есть много ответов, которые говорят вам, как сделать правильную копию, но ни один из них не говорит, почему ваша оригинальная «копия» не удалась.

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)

См. документацию для получения дополнительной информации об угловых случаях при копировании.

32 голосов
/ 10 апреля 2010

Использование thing[:]

>>> a = [1,2]
>>> b = a[:]
>>> a += [3]
>>> a
[1, 2, 3]
>>> b
[1, 2]
>>> 
30 голосов
/ 10 апреля 2010

Python идиома для этого: newList = oldList[:]

19 голосов
/ 05 апреля 2017

Python 3.6 Сроки

Вот результаты синхронизации с использованием Python 3.6.8. Имейте в виду, что это время относительно друг друга, а не абсолютное.

Я придерживался только мелкого копирования, а также добавил несколько новых методов, которые не были возможны в Python2, таких как list.copy() (эквивалент фрагмента Python3 ) и две формы списка распаковка (*new_list, = list и new_list = [*list]):

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

Мы можем видеть, что победитель Python2 по-прежнему преуспевает, но не сильно вытесняет Python3 list.copy(), особенно учитывая превосходную читаемость последнего.

Темная лошадка - это метод распаковки и повторной упаковки (b = [*a]), который на ~ 25% быстрее, чем нарезка сырой, и более чем в два раза быстрее, чем другой метод распаковки (*b, = a).

b = a * 1 также на удивление хорошо.

Обратите внимание, что эти методы не выводят эквивалентные результаты для любого ввода, кроме списков. Все они работают для срезаемых объектов, несколько работают для любого итерируемого, но только copy.copy() работает для более общих объектов Python.


Вот код тестирования для заинтересованных сторон ( Шаблон здесь ):

import timeit

COUNT = 50000000
print("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))
18 голосов
/ 13 ноября 2017

Давайте начнем с самого начала и исследуем его немного глубже:

Итак, предположим, у вас есть два списка:

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

И мы должны скопировать оба списка, теперь начиная с первого списка:

Итак, сначала давайте попробуем по общему методу копирования:

copy=list_1

Теперь, если вы думаете, что скопировал список_1, то вы можете ошибаться, давайте проверим это:

The id() function shows us that both variables point to the same list object, i.e. they share this object.
print(id(copy))
print(id(list_1))

выход:

4329485320
4329485320

Удивлен? Хорошо, давайте исследуем это:

Итак, как мы знаем, python ничего не хранит в переменной, переменные просто ссылаются на объект, а объект хранит значение. Здесь объект list, но мы создали две ссылки на этот же объект под двумя разными именами переменных. Таким образом, обе переменные указывают на один и тот же объект:

поэтому, когда вы делаете copy=list_1, что на самом деле делает:

enter image description here

Здесь на изображении list_1 и copy два имени переменных, но объект одинаков для обеих переменных, что составляет list

Таким образом, если вы попытаетесь изменить скопированный список, то он также изменит исходный список, поскольку список там только один, вы будете изменять этот список независимо от того, делаете ли вы это из скопированного списка или из исходного списка:

copy[0]="modify"

print(copy)
print(list_1)

выход:

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

Таким образом, он изменил исходный список:

Каково решение тогда?

Решение:

Теперь перейдем ко второму питоническому методу копирования списка:

copy_1=list_1[:]

Теперь этот метод исправляет то, с чем мы столкнулись в первом выпуске, давайте проверим это:

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

4338792136
4338791432

Итак, мы видим, что оба списка имеют разные идентификаторы, и это означает, что обе переменные указывают на разные объекты, поэтому на самом деле происходит следующее:

enter image description here

Теперь давайте попробуем изменить список и посмотрим, сталкиваемся ли мы с предыдущей проблемой:

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[:]

мы на самом деле копируем только внешний список, а не вложенный список, поэтому вложенный список является одним и тем же объектом для обоих списков, давайте проверим:

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

выход:

4329485832
4329485832

Так что на самом деле, когда мы делаем copy_2=list_2[:], вот что происходит:

enter image description here

Создает копию списка, но только копию внешнего списка, а не копию вложенного списка, вложенный список одинаков для обеих переменных, поэтому, если вы попытаетесь изменить вложенный список, он также изменит исходный список, потому что объект вложенного списка одинаковы для обоих вложенных списков.

Так в чем же решение?

Решение deep copy

from copy import deepcopy
deep=deepcopy(list_2)

Итак, давайте проверим это:

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

выход:

4322146056 4322148040

оба идентификатора различны, теперь давайте проверим идентификатор вложенного списка:

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

выход:

4322145992
4322145800

Как видите, оба идентификатора различны, поэтому мы можем предположить, что оба вложенных списка теперь указывают на разные объекты.

Итак, когда вы делаете deep=deepcopy(list_2), что на самом деле происходит:

enter image description here

Таким образом, оба вложенных списка указывают на разные объекты, и теперь у них есть отдельная копия вложенного списка.

Теперь давайте попробуем изменить вложенный список и посмотрим, решил ли он предыдущую проблему или нет:

так что если мы сделаем:

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

выход:

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

Итак, как вы можете видеть, он не изменил исходный вложенный список, он только изменил скопированный список.

Если вам нравится мой подробный ответ, дайте мне знать, проголосовав за него, если у вас есть какие-либо сомнения, поняли этот ответ, прокомментируйте:)

18 голосов
/ 10 июля 2015

Все остальные авторы дали замечательных ответов, которые работают, когда у вас есть одноуровневый (выровненный) список, однако из упомянутых методов только клонирование / копирование списка работает только copy.deepcopy(). и не указывать на вложенные list объекты при работе с многомерными вложенными списками (list of lists). В то время как Феликс Клинг ссылается на это в своем ответе, есть еще кое-что к проблеме и, возможно, обходной путь, использующий встроенные модули, которые могут оказаться более быстрой альтернативой deepcopy.

Хотя new_list = old_list[:], copy.copy(old_list)' и Py3k old_list.copy() работают для одноуровневых списков, они возвращаются к указанию на list объекты, вложенные в old_list и new_list, и изменяются на один из list объекты увековечены в другом.

Редактировать: Новая информация обнаружена

Как указали Аарон Холл и PM 2Ring с использованием eval() - это не только плохая идея, но и намного медленнее, чем copy.deepcopy().

Это означает, что для многомерных списков единственным вариантом является copy.deepcopy(). С учетом вышесказанного, это действительно не вариант, поскольку производительность снижается, когда вы пытаетесь использовать ее в многомерном массиве умеренного размера. Я попытался timeit использовать массив размером 42x42, что было неслыханно или даже слишком много для приложений биоинформатики, и я разочаровался в ожидании ответа и просто начал печатать мое редактирование этого сообщения.

Казалось бы, единственный реальный вариант - инициализировать несколько списков и работать с ними независимо. Если у кого-то есть какие-либо предложения относительно того, как обрабатывать многомерное копирование списков, это будет оценено.

Как уже заявляли другие, являются существенными проблемами производительности при использовании модуля copy и copy.deepcopy для многомерных списков ,

...