сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Асинхронность в Python

Структуры данных в Python: list, dict, set, tuple

Зачем это спрашивают

Структуры данных Python - ежедневка любой роли, и вопросы тут приземлённые: «почему set быстрее списка», «что не так с дефолтом x=[]». Проверяют, чувствуешь ли ты цену операций за синтаксическим сахаром.

// Половина «медленного питона» на код-ревью это x in list внутри цикла. Начни с этого.

list и deque: что почём

list - динамический массив: доступ по индексу и append в конец - O(1) амортизированно, вставка или удаление в начале - O(n), весь хвост сдвигается. Очередь на списке - квадратичная боль; для неё deque с O(1) на обоих концах.

Comprehensions - идиома и скорость: [f(x) for x in xs] быстрее цикла с append; генераторное выражение - то же, но лениво, без материализации списка.

// list.sort() сортирует на месте и возвращает None: a = a.sort() - потерянные данные. Новый список отдаёт sorted(a).

амортизированный O(1)
редкие дорогие переаллокации размазаны по серии дешёвых операций

dict и set: хеш-таблицы под капотом

dict и set - хеш-таблицы: доступ, вставка и проверка вхождения - O(1) в среднем. Отсюда главная оптимизация языка: x in list это O(n) перебор, x in set - O(1); замена списка на множество в цикле поиска превращает квадрат в линию.

Ключи dict и элементы set обязаны быть хешируемыми, то есть иммутабельными: tuple и str годятся, list и dict - нет. Иммутабельность неглубокая: кортеж со списком внутри нехешируем.

// dict с Python 3.7 сохраняет порядок вставки это гарантия языка, а не деталь реализации. set порядка не гарантирует никакого.

хешируемость
у объекта стабильный __hash__; условие для ключей dict и элементов set

Копии, ссылки и мутабельный дефолт

Присваивание не копирует: b = a - вторая ссылка на тот же список, правка b видна в a. copy() - поверхностная копия (вложенные объекты общие), deepcopy - рекурсивная.

Знаменитая ловушка - мутабельный дефолт аргумента: он вычисляется один раз при определении функции и живёт между вызовами:

def f(x=[]):
    x.append(1)
    return x

f()  # [1]
f()  # [1, 1] — список общий между вызовами

def g(x=None):
    x = [] if x is None else x  # правильный паттерн
поверхностная копия
новый контейнер, но вложенные объекты - те же ссылки

Как отвечать: «Что вернут два вызова функции с дефолтом x=[]?»

Первый - [1], второй - [1, 1]: дефолтное значение вычисляется один раз при определении функции и живёт в её объекте, так что все вызовы без аргумента делят один список. Правильный паттерн - x=None и создание списка внутри: x = [] if x is None else x. Та же ловушка прячется в dataclass с default=[] - там спасает field(default_factory=list). В живом коде мутабельных дефолтов в сигнатурах не бывает.

Точный вывод, механизм, канонический фикс и перенос на dataclass - вопрос закрыт со всеми его продолжениями.

На чём валят

  • x in list внутри цикла - O(n²) на ровном месте; set решает.
  • def f(items=[]) - список общий для всех вызовов, данные «протекают» между ними.
  • b = a для списка - не копия: правка b видна в a.
  • a = a.sort() - sort() возвращает None, данные потеряны; новый список - sorted(a).
  • Мутировать список во время итерации по нему - пропущенные элементы.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 19, остальные разбираются в тренажёре.

  1. #data_structures1 / 5
    Когда осмысленно взять tuple вместо list?
    A)Кортеж в большинстве ситуаций быстрее и удобнее списка, поэтому список можно почти не использовать
    B)Список превосходит кортеж по большинству параметров, а кортеж оставили для обратной совместимости
    C)Когда набор фиксирован и не должен меняться: запись/координаты, ключ словаря, защита от случайной мутации при передаче
    D)Когда элементов ровно два, потому что кортеж не способен хранить больше двух элементов
    показать ответ и разбор
    +C)Когда набор фиксирован и не должен меняться: запись/координаты, ключ словаря, защита от случайной мутации при передаче

    // разбор: Кортеж берут для неизменяемой упорядоченной записи: фиксированные поля (координаты, строка результата), ключ словаря или элемент множества (кортеж хешируем), защита данных от случайной мутации при передаче в функцию. Список — когда коллекция должна расти и меняться. Число элементов у кортежа не ограничено.

  2. #data_structures2 / 5
    Что дают collections.defaultdict и Counter по сравнению с обычным dict?
    A)Они заменяют обычный dict в большинстве задач и при этом работают в разы быстрее его
    B)Они автоматически сохраняют свои данные на диск между запусками программы, в отличие от обычного словаря, живущего в памяти
    C)Они сортируют ключи по алфавиту при каждой вставке, чего обычный неупорядоченный словарь делать не умеет
    D)defaultdict создаёт значение по умолчанию для нового ключа (нет KeyError), а Counter считает частоты элементов одной строкой
    показать ответ и разбор
    +D)defaultdict создаёт значение по умолчанию для нового ключа (нет KeyError), а Counter считает частоты элементов одной строкой

    // разбор: defaultdict(list или int) сам создаёт значение по умолчанию при первом обращении к новому ключу — не нужно проверять «есть ли ключ» перед добавлением, удобно для группировки и накопления. Counter принимает итерируемое и считает частоты, а most_common(k) отдаёт топ. Оба — обычные словари с удобным поведением, не быстрее и не персистентные.

  3. #data_structures3 / 5
    Что должно быть верно у объекта, чтобы его можно было положить в set или сделать ключом dict?
    A)Объект обязан быть числом или строкой — произвольные пользовательские объекты в set и ключи dict класть не получится
    B)Объект должен занимать не более фиксированного числа байт, иначе хеш-таблица не сможет корректно его у себя разместить
    C)Он должен быть хешируемым: иметь стабильный __hash__, согласованный с __eq__ — равные объекты обязаны иметь равный хеш
    D)Достаточно, чтобы объект был изменяемым — тогда set сможет обновлять его на месте при коллизиях хешей заметно быстрее
    показать ответ и разбор
    +C)Он должен быть хешируемым: иметь стабильный __hash__, согласованный с __eq__ — равные объекты обязаны иметь равный хеш

    // разбор: set и dict кладут объект в корзину по его __hash__ и различают объекты в корзине через __eq__. Требования: хеш стабилен всё время, пока объект в контейнере (поэтому изменяемые объекты не годятся), и согласован с равенством — если a==b, то hash(a)==hash(b). Свои классы класть можно, если корректно определить оба метода.

  4. #data_structures4 / 5
    sorted(data, key=...) в Python — устойчивая (stable) сортировка. Что это даёт на практике?
    A)Устойчивость означает, что итоговый результат сортировки вообще никак не зависит от исходного порядка элементов в переданной коллекции
    B)Элементы с равными ключами сохраняют исходный взаимный порядок — можно сортировать в несколько проходов по разным ключам
    C)Устойчивость обеспечивает, что сортировка данных выполняется за линейное время O(n), а не O(n log n)
    D)Это значит, что sorted не падает с ошибкой, даже если отдельные элементы коллекции несравнимы по типу
    показать ответ и разбор
    +B)Элементы с равными ключами сохраняют исходный взаимный порядок — можно сортировать в несколько проходов по разным ключам

    // разбор: Стабильная сортировка (в CPython — Timsort) сохраняет исходный взаимный порядок элементов с РАВНЫМИ ключами. Практическая польза: многоуровневую сортировку делают последовательными проходами от менее к более значимому ключу (сначала по имени, потом по дате — совпадающие даты останутся в порядке по имени) или задают составной ключ key=lambda x:(x.a, x.b). Устойчивость — про порядок равных, а не про сложность (O(n log n)) или независимость от входа.

  5. #data_structures5 / 5
    Чем tuple отличается от list в Python?
    A)tuple быстрее, потому что хранится на стеке, а не в куче
    B)tuple может хранить элементы разных типов, list — нет
    C)tuple неизменяем — элементы не заменить
    D)разницы нет — tuple просто другой синтаксис списка
    показать ответ и разбор
    +C)tuple неизменяем — элементы не заменить

    // разбор: tuple после создания не меняется: нельзя заменить, добавить или удалить элемент — t[0] = 1 даёт TypeError. Следствия практичны: tuple хешируем (годится ключом dict и элементом set), защищает данные от случайной правки и компактнее в памяти. Изменяемая последовательность — list, «замороженная» — tuple.

дальше

Теорию прочитали. Навык ставится повторением

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