Методы списков в Python: добавление, сортировка, удаление и копирование

Обновлено:

Список — изменяемый объект, поэтому многие методы класса list работают с самим существующим списком, а не создают новый.

В этой теме разберём:

count()
index()

append()
insert()
extend()

reverse()
sort()

pop()
remove()
clear()

copy()

count()

1. Подсчёт элементов

С методом:

count()

мы уже знакомы по строкам.

У списка count() считает, сколько элементов списка равны переданному объекту.

numbers = [
    10,
    20,
    10,
    30,
    10,
]

result = numbers.count(10)

print(result)

Результат:

3

Если подходящих элементов нет:

print(numbers.count(100))

Результат:

0

Метод всегда возвращает:

int

2. count() сравнивает элементы целиком

У строк:

text.count('Py')

можно искать подстроку внутри строки.

В списке поиск устроен иначе:

technologies = [
    'Python',
    'Python backend',
    'SQL',
]

print(
    technologies.count('Python')
)

Результат:

1

Строка:

Python

внутри:

Python backend

не считается отдельным элементом списка.

Метод сравнивает объект, который мы передали, с отдельными элементами списка.


3. У list.count() нет start и stop

У строкового метода count() мы могли ограничивать область поиска:

text.count(
    'Python',
    start,
    end,
)

У списка метод проще:

list.count(value)

Передаётся только искомый объект.


index()

4. Поиск индекса элемента

Метод:

index()

возвращает индекс первого подходящего элемента.

numbers = [
    10,
    20,
    30,
    20,
    40,
]

index = numbers.index(20)

print(index)

Результат:

1

Хотя 20 встречается дважды, возвращается индекс первого совпадения.


5. Можно указать start и stop

Например:

numbers = [
    10,
    20,
    30,
    20,
    40,
]

print(
    numbers.index(
        20,
        2,
    )
)

Поиск начинается с индекса 2.

Результат:

3

Можно указать обе границы:

numbers.index(
    20,
    start,
    stop,
)

stop, как и в срезах, является невключительной правой границей.


6. Если элемента нет

numbers = [
    10,
    20,
    30,
]

numbers.index(100)

возникнет:

ValueError

То есть list.index() ведёт себя в этом отношении так же, как строковый:

str.index()

7. У списков нет find() и rfind()

У строк мы изучали:

find()
rfind()
index()
rindex()

У списка существуют:

index()

но нет строковых методов:

find()
rfind()
rindex()

Если нужно найти первый индекс элемента списка, используем:

list.index()

append()

8. Добавление одного объекта в конец

Метод:

append()

добавляет ровно один объект в конец списка.

technologies = [
    'Python',
    'SQL',
]

technologies.append('Django')

print(technologies)

Результат:

['Python', 'SQL', 'Django']

9. Добавить можно любой объект

Например:

data = [
    'Python',
    'SQL',
]

data.append(100)
data.append(None)

print(data)

Результат:

['Python', 'SQL', 100, None]

Можно добавить даже другой список:

numbers = [
    1,
    2,
]

numbers.append(
    [3, 4]
)

print(numbers)

Результат:

[1, 2, [3, 4]]

Добавился один объект — вложенный список.


append() изменяет существующий список

10. id() остаётся прежним

numbers = [
    10,
    20,
]

print(id(numbers))

numbers.append(30)

print(id(numbers))

Идентификатор списка остаётся тем же.

Мы изменили:

[10, 20]

на:

[10, 20, 30]

но объект list остался прежним.


insert()

11. Вставка по указанному индексу

Метод:

insert()

принимает:

list.insert(index, object)

Например:

technologies = [
    'Python',
    'Django',
]

technologies.insert(
    1,
    'SQL',
)

print(technologies)

Результат:

['Python', 'SQL', 'Django']

Новый объект вставился под индексом 1.

Существующие элементы справа были сдвинуты.


12. Вставка в начало

numbers = [
    20,
    30,
]

numbers.insert(
    0,
    10,
)

print(numbers)

Результат:

[10, 20, 30]

13. Большой индекс не вызывает IndexError

numbers = [
    10,
    20,
    30,
]

numbers.insert(
    1000,
    40,
)

print(numbers)

Результат:

[10, 20, 30, 40]

Если положительный индекс находится значительно правее конца списка, элемент просто вставляется в конец.

А очень маленький отрицательный индекс будет ограничен началом списка.


append() и insert()

14. Стоимость операций

Вспоминаем устройство списка CPython как динамического массива ссылок.

При:

numbers.append(value)

элемент добавляется в конец.

Если во внутреннем массиве есть резервная вместимость, существующие элементы перемещать не требуется.

Поэтому append() имеет:

amortized O(1)

При:

numbers.insert(
    0,
    value,
)

нужно сдвинуть существующие элементы:

до:

[A][B][C][D]

после:

[X][A][B][C][D]

Поэтому вставка в начало:

O(n)

Чем ближе позиция вставки к концу списка, тем меньше элементов приходится сдвигать.


15. Небольшой эксперимент

Можно сравнить работу с концом и началом большого списка:

from timeit import timeit


numbers = list(range(100_000))


def work_with_end():
    numbers.append(999)
    numbers.pop()


def work_with_start():
    numbers.insert(0, 999)
    del numbers[0]


print(
    timeit(
        work_with_end,
        number=10_000,
    )
)

print(
    timeit(
        work_with_start,
        number=10_000,
    )
)

Конкретные числа зависят от компьютера, версии Python и размера списка.

Главное здесь не конкретное количество секунд, а разница алгоритмов:

конец списка   → обычно O(1)
начало списка  → O(n)

extend()

16. Добавление элементов из итерируемого объекта

Метод:

extend()

тоже добавляет элементы в конец списка.

Но он принимает итерируемый объект и добавляет его элементы по одному.

first_numbers = [
    10,
    20,
]

second_numbers = [
    30,
    40,
]

first_numbers.extend(
    second_numbers
)

print(first_numbers)

Результат:

[10, 20, 30, 40]

append() и extend()

17. Главное различие

numbers = [
    1,
    2,
]

numbers.append(
    [3, 4]
)

Результат:

[1, 2, [3, 4]]

Добавили один объект.


А:

numbers = [
    1,
    2,
]

numbers.extend(
    [3, 4]
)

Результат:

[1, 2, 3, 4]

extend() обошёл переданный список и добавил его элементы.


18. Кортеж

numbers = [
    1,
    2,
]

numbers.extend(
    (3, 4, 5)
)

print(numbers)

Результат:

[1, 2, 3, 4, 5]

19. Строка

Строка является итерируемым объектом:

letters = [
    'A',
    'B',
]

letters.extend(
    'XYZ'
)

print(letters)

Результат:

['A', 'B', 'X', 'Y', 'Z']

Каждый символ добавился как отдельный элемент.

Сравним с:

letters = [
    'A',
    'B',
]

letters.append(
    'XYZ'
)

print(letters)

Результат:

['A', 'B', 'XYZ']

20. int передать в extend() нельзя

numbers = [
    1,
    2,
]

numbers.extend(100)

Возникнет:

TypeError

Потому что int не является итерируемым объектом.

При этом:

numbers.append(100)

работает нормально, потому что append() просто добавляет переданный объект целиком.


reverse()

21. Разворот существующего списка

numbers = [
    10,
    20,
    30,
    40,
]

numbers.reverse()

print(numbers)

Результат:

[40, 30, 20, 10]

Метод изменяет существующий список.

Новый список не создаётся.


reverse() и [::-1]

22. Срез создаёт новый список

numbers = [
    10,
    20,
    30,
]

reversed_numbers = (
    numbers[::-1]
)

print(numbers)
print(reversed_numbers)

Результат:

[10, 20, 30]
[30, 20, 10]

Получили два разных списка.


А:

numbers = [
    10,
    20,
    30,
]

numbers.reverse()

изменяет сам:

numbers

23. Сложность обоих способов

И:

numbers[::-1]

и:

numbers.reverse()

имеют линейную сложность:

O(n)

Но [::-1]:

  • проходит по элементам;
  • создаёт новый список;
  • выделяет память под новый массив ссылок.

reverse():

  • работает с существующим списком;
  • не создаёт второй список;
  • переставляет ссылки внутри него.

Поэтому, если новый список не нужен, reverse() обычно является более подходящей операцией.

Конкретная разница во времени зависит от версии Python, размера данных и компьютера.


24. Идея алгоритма reverse()

Упрощённо разворот можно представить так:

[A][B][C][D][E]

поменять:
A ↔ E

[B] ↔ [D]

C остаётся на месте

То есть достаточно двигаться от двух краёв к центру.

Например, учебный вариант:

def my_reverse(values):
    last_index = len(values) - 1

    for index in range(
        len(values) // 2
    ):
        opposite_index = (
            last_index - index
        )

        (
            values[index],
            values[opposite_index],
        ) = (
            values[opposite_index],
            values[index],
        )

С циклами подробно познакомимся позже.

Сейчас важна сама идея:

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


sort()

25. Сначала вспомним sorted()

Функция:

sorted()

принимает итерируемый объект и возвращает новый список.

numbers = [
    30,
    10,
    20,
]

sorted_numbers = sorted(
    numbers
)

print(numbers)
print(sorted_numbers)

Результат:

[30, 10, 20]
[10, 20, 30]

Исходный список не изменился.


Метод sort()

26. Сортировка на месте

numbers = [
    30,
    10,
    20,
]

numbers.sort()

print(numbers)

Результат:

[10, 20, 30]

Метод изменил существующий список.


27. sort() существует именно у списков

Например:

numbers = (
    30,
    10,
    20,
)

У tuple нет:

numbers.sort()

Для таких объектов можно использовать:

sorted(numbers)

которая вернёт новый список.


28. Обратная сортировка

Как и у sorted(), можно использовать:

reverse=True
numbers = [
    30,
    10,
    20,
]

numbers.sort(
    reverse=True
)

print(numbers)

Результат:

[30, 20, 10]

pop()

29. Удалить и получить элемент

Мы уже можем удалить элемент:

del numbers[-1]

Но иногда удаляемое значение нужно получить.

Для этого используется:

pop()
tasks = [
    'task_1',
    'task_2',
    'task_3',
]

last_task = tasks.pop()

print(last_task)
print(tasks)

Результат:

task_3
['task_1', 'task_2']

Метод одновременно:

  1. удалил элемент;
  2. вернул его.

30. По умолчанию удаляется последний элемент

list.pop()

по умолчанию работает с индексом:

-1

Работа с концом динамического массива является эффективной операцией:

O(1)

31. Практический пример

Предположим, список хранит задачи, которые нужно обработать с конца:

tasks = [
    'send_email',
    'create_report',
    'backup_database',
]

current_task = tasks.pop()

print(
    f'Выполняем: {current_task}'
)

print(
    f'Осталось: {tasks}'
)

Мы одновременно забрали задачу для работы и удалили её из списка ожидающих задач.


32. Можно передать индекс

numbers = [
    10,
    20,
    30,
    40,
]

removed_number = numbers.pop(1)

print(removed_number)
print(numbers)

Результат:

20
[10, 30, 40]

Если удаляется элемент не с конца, элементы справа приходится сдвигать.


33. Ошибки pop()

Несуществующий индекс:

numbers = [
    10,
    20,
]

numbers.pop(100)

приведёт к:

IndexError

Пустой список:

numbers = []

numbers.pop()

также приведёт к:

IndexError

remove()

34. Удаление по значению

pop() принимает индекс.

remove() принимает значение объекта, который нужно удалить.

technologies = [
    'Python',
    'SQL',
    'Django',
]

technologies.remove(
    'SQL'
)

print(technologies)

Результат:

['Python', 'Django']

35. Удаляется первое совпадение

numbers = [
    10,
    20,
    10,
    30,
]

numbers.remove(10)

print(numbers)

Результат:

[20, 10, 30]

Удалено только первое значение 10.


36. Почему remove() может быть дорогим

Чтобы выполнить:

numbers.remove(value)

сначала необходимо найти первое равное значение.

Упрощённо:

index 0 → подходит?
index 1 → подходит?
index 2 → подходит?
...

Поиск может потребовать обхода значительной части списка.

После удаления элементы справа ещё нужно сдвинуть.

Поэтому remove() имеет сложность:

O(n)

37. Если значения нет

numbers = [
    10,
    20,
    30,
]

numbers.remove(100)

возникнет:

ValueError

clear()

38. Очистить содержимое списка

numbers = [
    10,
    20,
    30,
]

numbers.clear()

print(numbers)

Результат:

[]

Сам объект списка продолжает существовать.


39. clear() и del — разные действия

numbers = [
    10,
    20,
    30,
]

numbers.clear()

После этого имя:

numbers

по-прежнему существует и ссылается на пустой список.


А:

numbers = [
    10,
    20,
    30,
]

del numbers

удаляет само имя:

numbers

из текущей области видимости.

После этого:

print(numbers)

приведёт к:

NameError

Методы, которые возвращают None

40. Очень важное правило

Большинство методов, которые изменяют список на месте, возвращают:

None

Например:

numbers = [
    10,
    20,
]

result = numbers.append(30)

print(numbers)
print(result)

Результат:

[10, 20, 30]
None

41. Нельзя писать так

numbers = [
    10,
    20,
]

numbers = numbers.append(30)

После выполнения:

numbers

будет ссылаться на:

None

Мы потеряли через это имя доступ к списку.

Правильно:

numbers.append(30)

42. Какие методы изменяют список и возвращают None

Из изученных:

append()
insert()
extend()

reverse()
sort()

remove()
clear()

они изменяют список и возвращают:

None

Это хорошо показывает намерение:

Метод вызван ради изменения существующего объекта.


Методы с другим поведением

43. count() и index()

Они список не изменяют.

numbers.count(10)

возвращает количество.

numbers.index(10)

возвращает индекс.


44. pop()

Особый случай:

value = numbers.pop()

Метод:

  • изменяет список;
  • возвращает удалённый объект.

45. copy()

Тоже возвращает значение:

new_list = numbers.copy()

Потому что задача метода — создать другой список.


copy()

46. Поверхностная копия

original = [
    10,
    20,
    30,
]

copied = original.copy()

print(original)
print(copied)

print(
    original is copied
)

Результат:

[10, 20, 30]
[10, 20, 30]
False

Это два разных объекта list.


47. Изменение внешнего списка независимо

original = [
    10,
    20,
    30,
]

copied = original.copy()

original.append(40)

print(original)
print(copied)

Результат:

[10, 20, 30, 40]
[10, 20, 30]

Добавление в один внешний список не изменило второй.


Поверхностная копия

48. Что именно копирует copy()

Внутри списка хранятся ссылки на объекты.

Представим:

original = [
    object_1,
    object_2,
    object_3,
]

После:

copied = original.copy()

создаётся новый внешний список, но в него копируются ссылки на те же элементы:

original
  ↓
[ref A][ref B][ref C]
   ↓      ↓      ↓
   A      B      C

copied
  ↓
[ref A][ref B][ref C]
   ↓      ↓      ↓
   A      B      C

Это называется:

shallow copy — поверхностная копия.


Вложенный изменяемый объект

49. Важный пример

original = [
    [10, 20],
    [30, 40],
]

copied = original.copy()

Внешние списки разные:

print(
    original is copied
)

Результат:

False

Но:

print(
    original[0] is copied[0]
)

Результат:

True

Оба внешних списка содержат ссылки на один и тот же вложенный список.


50. Поэтому изменение вложенного списка видно в обеих копиях

original = [
    [10, 20],
    [30, 40],
]

copied = original.copy()

original[0].append(99)

print(original)
print(copied)

Результат:

[[10, 20, 99], [30, 40]]
[[10, 20, 99], [30, 40]]

Мы изменили сам вложенный объект.

А ссылка на него находится в обоих внешних списках.


deepcopy()

51. Глубокая копия

Для рекурсивного копирования составной структуры существует функция:

deepcopy()

из модуля:

copy

Пример:

from copy import deepcopy


original = [
    [10, 20],
    [30, 40],
]

copied = deepcopy(
    original
)

original[0].append(99)

print(original)
print(copied)

Результат:

[[10, 20, 99], [30, 40]]
[[10, 20], [30, 40]]

Теперь вложенные изменяемые структуры тоже были скопированы, поэтому изменение вложенного списка original не затронуло copied.


52. copy() и deepcopy()

Упрощённо:

copy()
↓
новый внешний контейнер
внутренние объекты остаются общими

deepcopy()
↓
рекурсивно копирует составную структуру

deepcopy() нужен не всегда.

Для обычного плоского списка:

numbers = [
    10,
    20,
    30,
]

обычного:

numbers.copy()

обычно достаточно.


Краткая таблица методов list

Метод Что делает
list.count(value) Возвращает количество элементов, равных value.
list.index(value, start=..., stop=...) Возвращает индекс первого найденного value; если элемента нет — ValueError.
list.append(object) Добавляет один объект в конец списка.
list.insert(index, object) Вставляет один объект перед указанным индексом.
list.extend(iterable) Добавляет в конец элементы из итерируемого объекта.
list.reverse() Разворачивает существующий список на месте.
list.sort(reverse=False) Сортирует существующий список на месте.
list.pop(index=-1) Удаляет элемент по индексу и возвращает его.
list.remove(value) Удаляет первое вхождение указанного значения.
list.clear() Удаляет все элементы из существующего списка.
list.copy() Возвращает поверхностную копию списка.

Главное по теме

Методы:

append()
insert()
extend()
reverse()
sort()
remove()
clear()

изменяют существующий список и возвращают:

None

Поэтому:

numbers.append(10)

а не:

numbers = numbers.append(10)

Методы:

count()
index()

список не изменяют, а возвращают информацию о его элементах.


Метод:

pop()

одновременно:

  • изменяет список;
  • возвращает удалённый элемент.

Метод:

copy()

возвращает новый список, но выполняет только поверхностное копирование.

Для независимого копирования вложенных изменяемых структур существует:

from copy import deepcopy

С точки зрения производительности особенно полезно помнить:

append() в конец      → amortized O(1)
pop() с конца         → O(1)

insert() в начало     → O(n)
remove()              → O(n)
pop(0)                → O(n)

Поэтому динамический массив особенно эффективен для работы с концом списка.