Списки (list)

Список - это упорядоченная последовательность из нуля или более ссылок на объекты. Относятся к категории изменяемых (mutable) объектов, потому возможно замещать, удалять и добавлять новые элементы (или целые срезы элементов). Т.е списки динамические. Списки лучше всего понимаются если рассматривать их как ассоциативные массивы (Интерпретатор Cpython в целом так и работает со списками).


Создание списка

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

Создадим список: l = ['text', ['a', 'b'], 1, 2, 3 ]
Чтобы создать срез списка, следует задать индексы первого и последнего элементов, с которыми вы намереваетесь работать. Как и в случае с функцией range() , Python останавливается на элементе, предшествующем второму индексу.

Рассмотрим ряд примеров со списком игроков команды:
players = ['00 Стюарт', '01 Холувей', '02 Месси', '03 Флоренс', '04 Али']
print('1)', players[0:3])
print('2)', players[1:4])
print('3)', players[:4])
print('4)', players[2:])
print('5)', players[-3:])
print('6)', players[::2])
Рассмотрим подробно каждый пример среза:

  1. Вывод сохраняет структуру списка, но включает только первых трех игроков.
  2. Подмножество может включать любую часть списка. Например, чтобы ограничиться вторым, третьим и четвертым элементами списка, срез начинается с индекса 1 и заканчивается на индексе 4.
  3. Если первый индекс среза не указан, то Python автоматически начинает срез от ­начала списка.
  4. Аналогичный синтаксис работает и для срезов, включающих конец списка. Например, если вам нужны все элементы с третьего до последнего, начните с индекса 2 и не указывайте второй индекс.
  5. Вспомните, что отрицательный индекс возвращает элемент, находящийся на заданном расстоянии от конца списка; следовательно, вы можете получить любой срез от конца списка. Например, чтобы отобрать последних трех игроков.
  6. Третий не обязательный аргумент - это шаг среза, укажем шаг 2 чтобы получить все элементы с четным индексом.
При указании не существующего индекса интерпретатор породит исключение IndexError, рассмотрим пример:
players, command = [], []
# ввод списка игроков
while True:
    try:
        players.append(input())
    except (EOFError, KeyboardInterrupt):
        break
    
# сбор команды
command = players[:11] # не вызовет исключения
goalkeeper = players[10] # вызывет исключение IndexError
print('Наш смелый вратарь:', goalkeeper)
В примере мы смогли организовать ввод неорпеделенного кол-ва игроков из консоли, затем после ввода последнего игрока мы утверждаем команду из 11 человек и назначаем одиннацатого участника вратарем. Если мы вводим кол-во участников < 11 то опирация среза не вызывет исключения, а вернет столько игроков, сколько есть. Но при обращении по индексу players[10], в случае отсутствия такого индекса - всегда поднимается исключение IndexError, давайте его обработаем:
players, command = [], []
# ввод списка игроков
while True:
    try:
        players.append(input())
    except (EOFError, KeyboardInterrupt):
        break
    
# сбор команды
try:
    goalkeeper = players[10] # вызывет исключение IndexError
except IndexError:
    goalkeeper = 'Кажется мы играем без вратаря =('
else:
    command = players[:11] # не вызовет исключения
finally:
    print('Наш смелый вратарь:', goalkeeper)
Методы
Операции
Давайте разберем на примере методы и операции работы со списками.

Пример: Давайте смоделируем работу цеха по переработке бытовых отходов. На вход подается неопределенное кол-во единиц мусора, а на выходе нужно получить список из трех элементов: бумага, металл и пластик.
# определим типы отходов
coin = 'монетка'
potatoes = 'гнилая картошка'
papers = ('картон', 'бумага', 'паспорт') # уместное использование кортежей
metals = ('консерва', 'вилка', coin, 'бп')
plastics = ('бутылка', 'ведро')
# Открываем конейнеры для отходов
# Металл, пластик, другие отходы будем сваливать в кучу
paper_trash, metal_trash, plastic_trash, undefined_trash = [], [], [], [] 
# Мусоровоз приехал и конвеер запускается
while True:
    try:
        trash = input()
        if trash in papers:
            paper_trash.append(trash)
        elif trash in metals:
            metal_trash.append(trash)
        elif trash in plastics:
            plastic_trash.append(trash)
        elif trash in metals:
            metal_trash.append(trash)
        else:
            undefined_trash.append(trash)
    except (EOFError, KeyboardInterrupt):
        break

print('Содержимое баков:', metal_trash, plastic_trash, undefined_trash)
# Конвеер рассортировал мусор по бакам
# Попробуем найти в баке с металлом что-то полезное, например монетку
try:
    coin_index = metal_trash.index(coin)
    my_coin = metal_trash.pop(coin_index)
    print(f'Ура, я нашел: "{my_coin}"')
    print('Содержимое бака с металлом после изьятия монетки:', metal_trash)
except ValueError:
    print('В этор раз ничего полезного =(')

# ------------ Обработка металла
# отсортируем металлический мусор по размеру:
metal_trash.sort(key=lambda trash: len(trash))
print('Содержимое бака с металлом после сортировки по возрастанию:', metal_trash)
metal_trash.sort(key=lambda trash: len(trash), reverse=True)
print('Содержимое бака с металлом после сортировки по убыванию:', metal_trash)
# прессуем металлический мусор
metal_ball = '+'.join(metal_trash)
print(f'Вот такой получился комок металла: "{metal_ball}"')
metal_trash = [metal_ball] # закинем обратно в бак

# ------------ Обработка бумаги
# Отправляем бумажный мусор под пресс. В один момент времени пресс может сжать только одну единицу мусора
# В итоге должен получиться один большой комок "paper_ball"
paper_ball = ''
while paper_trash:
    last_trash = paper_trash.pop()
    paper_ball += last_trash # пресуем в комок очередной мусор
print(f'Вот такой получился комок бумаги: "{paper_ball}"')
paper_trash.append(paper_ball) # закинем обратно в бак

# ------------ Обработка пластика
plastic_ball = '+'.join(plastic_trash)
plastic_trash = [plastic_ball] # закинем обратно в бак
print(f'Вот такой получился комок пластика: "{plastic_ball}"')

# ------------ Другие отходы просто сожгем вместе с баком
# Узнаем для статистики, сколько в баке кг гнилой картошки
print(f'В других отходах найдено: {undefined_trash.count(potatoes)} кг. гнилой картошки')
undefined_trash = []

# Теперь загрузим отсортированные отходы и увезем на переработку
result = paper_trash + metal_trash + plastic_trash
print('Содержимое фургона с отсортированными отходами', result)
Генераторы списков
Генераторы коллекций предназначены для компактного программного заполнения коллекций.

Важно не путать понятие "герератор коллекции" и "выражение-генератор" (generator expression). Выражение генератор создает на каждом шаге итерации следующий элемент согласно некоторым правилам, о них поговорим отдельно. Сейчас будем говорить о генератре коллекции, а именно списка.

Пример: Необходимо заполнить список последовательностью чисел 1, 2, 3, ... N. Для программного создания списка можно использовать инструкцию цикла с условием:
result = []
for i in range(10):
    if i != 5:    
        result.append(i)
print(result)
Но данная конструкция при своей простоте весьма объемна и ее можно сократить используя генератор списка.

Генератор списка - это выражение, заключенное в квадратные скобки, которое состоит из цикла по итерируему объекту и условия для исключения нежелательных элементов.
result = [i for i in range(10) if i != 5]
print(result)
Структура выражения генератора списка:

[ < элемент > for < элемент > in < итерируемый объект > if < условие > ]

При этом условие является необязательным элементом, генератор может быть безусловным:
l = ['0', '1', '2', '3', '4', '5']
print(l[:2])
print(l[:6:2])
Дополнительные варианты использования генератора списка

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

Структура: [ < значение 1 > if < условие на элемента > else < значение 2 > for < элемент > in < итерируемый объект > ]
colors = ['white', 'orange', 'blue']
data = ['2', False, 'white', None, 'orange', 'text'] 
data_filter = ['color' if elem in colors else None for elem in data ]
print('Список случайных элементов: ', data)
print('Какие элементы списка цвета:', data_filter)
2. Использовать функцию для преобразования элемента перед добавлением в список:

[function(< значение 1 >) for < элемент > in < итерируемый объект > ]
colors = ['white', 'orange', 'blue']
data = ['2', False, 'white', None, 'orange', 'text'] 

def is_color(elem) -> bool:
    return elem in colors
    
data_filter = [is_color(elem) for elem in data ]
print('Список случайных элементов: ', data)
print('Какие элементы списка цвета:', data_filter)
3. Генераторы могут быть вложенными, но будьте осторожны, т.к. это снижает читаемость кода, в некоторых случаях будет лучше использовать обычную инструкцию цикла с условием - если это повысит читаемость кода.
rows_num = 5
cols_num = 10
matrix = [[i for i in range(rows_num)] for col in range(cols_num)]

for col in matrix:
    print(col)
Что нужно знать о списках
  1. Списки относятся к категории изменяемых объектов, поэтому они поддерживают операции, которые изменяют сам список непосредственно. То есть все операции, представленные в этом разделе, изменяют сам список объектов и не приводят к необходимости создавать новую копию, как это было в случае со строками. Это очень важный нюанс, т.к. другие переменные ссылающиеся на список также будут изменены, такое поведение часто приводит к ошибкам.
  2. Методы append и sort изменяют сам объект списка и не возвращают список в виде результата (точнее говоря, оба метода возвращают значение None).
  3. Методы append и pop позволяют реализовать стек (LIFO).
  4. Лучшая производительность обеспечивается при добавлении и удалении элемента из конца списка append и pop. Падение производительности происходит когда нужно искать элемент в середине списка, например с помощью remove, index, count и оператора членства in. Для более быстрой работы с произвольными элементами лучше использовать множества и словари либо хранить список в отсортированном виде.
  5. Будьте осторожны с большими списками. Если сгенерированный список получается слишком большим или неизвестен максимальный размер списка необходимо понимать что создание такого списка будет стоить больше памяти, чем больше ваш список, т.к. его нужно где-то хранить. Это очень важная проблема и для таких случаев следует использовать выражения-генераторы, которые не хранят статичный список в памяти, а на генерируют значение на каждом шаге итерации. Выражения-генераторы будут рассмотрены в отдельной главе.
  6. Удаление. Если удаляется последняя ссылка на объект пустого списка в памяти, то он не удаляется, и может быть пере использован в дальнейшем.
  7. Изменение размера. Чтобы избежать накладные расходы на постоянное изменение размера списков, Python не изменяет его размер каждый раз, как только это требуется. Вместо этого, в каждом списке есть набор дополнительных ячеек, которые скрыты для пользователя, но в дальнейшем могут быть использованы для новых элементов. Как только скрытые ячейки заканчиваются, Python добавляет дополнительное место под новые элементы. Причём делает это с хорошим запасом, количество скрытых ячеек выбирается на основе текущего размера списка — чем он больше, тем больше дополнительных скрытых слотов под новые элементы. Эта оптимизация особенно выручает, когда вы пытайтесь добавлять множество элементов в цикле. Паттерн роста размера списка выглядит примерно так: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88,…
  8. Поиск. Поиск элементов в списке осуществляется за линейное время O(n). Это значит, что с увеличением списка, время работы будет расти пропорционально. Если список отсортирован то поиск происходит за логарифмическое время O(log).