open atlas
↑ К треку
Python для JS/TS-разработчиков PY · 01 · 02

Списки, словари, множества, кортежи

В Python четыре базовых контейнера — list, dict, set, tuple. Выбор правильного — это вопрос изменяемости и хешируемости, а ошибка стоит O(n) там, где O(1) был бесплатным.

PY Middle ◷ 17 min
Уровень
ОсновыJuniorMiddleSenior

Ревьюер помечает твою функцию: на каждый запрос она сканирует list из 50 000 элементов через if user_id in seen, и эндпойнт еле ползёт. Ты написал это как в JS — массив, цикл, по духу .includes(). Фикс — один символ намерения: сделай seen множеством (set). Проверка вхождения падает с O(n) до O(1), и p99 схлопывается. Те же данные, та же форма кода — другой контейнер, потому что в Python контейнер и есть решение о производительности. Хвататься за list рефлекторно, как за [] в JavaScript, — самый частый способ написать случайно квадратичный Python.

Всё решают два вопроса: изменяемый? хешируемый?

Python даёт четыре встроенных контейнера, и выбор между ними — не вкусовщина, а два вопроса «да/нет». Изменяемый ли он? — можно ли поменять его на месте после создания. Хешируемый ли он? — может ли Python вычислить стабильный хеш, чтобы объект стал ключом dict или членом set. Эти свойства связаны: объект хешируем, только если его значение не может измениться под хешем, поэтому изменяемые встроенные типы не хешируемы. Это единственное правило объясняет, почему list не может быть ключом dict, а tuple может.

  • list — упорядоченный, изменяемый, рабочая лошадка-последовательность. Ближе всего к JS Array, но методы другие (.append(), а не .push(); .sort() меняет на месте и возвращает None). Добавление в конец — амортизированный O(1); insert(0, x), del lst[0] и x in lst — все O(n). Не хешируем.
  • dict — хеш-таблица с гарантированным порядком вставки начиная с Python 3.7. Ключи должны быть хешируемыми; поиск, вставка и удаление — O(1) в среднем. Это объединённые в один тип JS Object и Map: и со строковыми ключами, и с произвольными хешируемыми ключами, упорядоченный, быстрый.
  • set — неупорядоченная коллекция уникальных хешируемых членов. O(1) на вхождение, плюс первоклассные | объединение, & пересечение, - разность. Как JS Set, но с настоящей алгеброй множеств из коробки.
  • tuple — упорядоченный, неизменяемый и хешируемый, если хешируем каждый элемент. В JavaScript эквивалента нет. Это тип для фиксированных записей, множественного возврата и — что важно — составных ключей dict/set.

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

point = (3, 4)              # tuple: fixed record, hashable
grid = {point: "origin"}   # tuple as a dict key — works
grid[[3, 4]]               # TypeError: unhashable type: 'list'

x, y = point               # unpacking a tuple into two names
def bounds(): return 0, 100  # returning a tuple (the comma makes it)
lo, hi = bounds()

Шпаргалка и перевод с JS

Когда приходишь из JavaScript, ошибка не в синтаксисе — в том, что Array натягивают на всё. JS заставляет имитировать множество объектом ({}) или Map, имитировать алгебру множеств фильтрами и вовсе не имеет неизменяемой записи. Python раскладывает эти задачи по четырём специализированным типам, и цена неправильного выбора — реальная алгоритмическая сложность, а не просто стиль.

ТипИзменяемый?Упорядоченный?Хешируемый?Бери его, когда…Аналог в JS
listдаданетупорядоченная растущая последовательность для итерации/индексацииArray
dictдада (вставка)нетпоиск ключ→значение, подсчёт, группировка, любое отображениеObject / Map
setданетнетпроверки вхождения, дедупликация, объединение/пересечениеSet
tupleнетдада*фиксированные записи, множественный возврат, составные ключи dict/set(нет)

Звёздочка у tuple важна: кортеж хешируем, только если хешируемы все его элементы. (1, 2) — нормальный ключ; (1, [2]) — нет, потому что внутри вложен list. Хешируемость рекурсивна.

Выбери лучший вариант

Ты обрабатываешь поток событий и миллионы раз отвечаешь на вопрос «видел ли я уже пару (user_id, day)?». Выбери структуру для хранения виденных пар.

Идентичность vs равенство и ловушка изменяемого дефолта

Две ловушки бьют инженеров, переходящих из JS. Первая — идентичность (is) против равенства (==). == сравнивает значения; is сравнивает идентичность объекта (тот же объект в памяти) и именно его надо использовать в проверках None/True/False. Они расходятся так, что выглядит магией: [1,2] == [1,2] даёт True, а [1,2] is [1,2]False (два разных списка). CPython кэширует малые целые и короткие строки, поэтому 256 is 256 может быть True, а 257 is 257False: деталь реализации, на которую нельзя полагаться. Правило: == для проверок значений, is — только для None и других синглтонов.

Вторая — изменяемый аргумент по умолчанию. Значение по умолчанию вычисляется один раз, при определении функции, и разделяется между всеми вызовами — поэтому изменяемый дефолт вроде [] или {} накапливает состояние между вызовами, и баг выглядит как наваждение.

def add(item, bucket=[]):     # bucket is created ONCE, shared forever
    bucket.append(item)
    return bucket

add("a")        # ['a']
add("b")        # ['a', 'b']  ← surprise: the same list persisted

def add(item, bucket=None):   # the fix: sentinel + fresh object per call
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket
Почему это работает

Присваивание в Python связывает имя с объектом; оно никогда не копирует. Поэтому b = a для списка означает, что a и bодин и тот же список, и b.append(1) меняет оба. Копирование явное: a.copy() (или a[:], list(a)) делает поверхностную копию — новый внешний контейнер с теми же внутренними объектами, так что мутация вложенного списка по-прежнему видна через оба. Для полностью независимого клона вложенных данных бери copy.deepcopy(). Это та же модель ссылочной семантики, что у объектов и массивов в JS, — ловушка в допущении, что = дублирует.

Викторина

Почему `d = {[1, 2]: 'x'}` вызывает TypeError, а `d = {(1, 2): 'x'}` работает?

Викторина

Проверка вхождения `if x in c` бежит миллионы раз в горячем цикле. Какой контейнер держит её O(1)?

Расставь шаги по порядку

Функция должна записать каждую уникальную пару (user, action) один раз и быстро отвечать на вхождение. Расставь решение от главного вопроса вниз до финального выбора:

  1. 1 Нужно быстрое вхождение / уникальность? → да, значит хеш-контейнер (set/dict), а не list
  2. 2 Это чистое вхождение (видел или нет) или я храню значение? → видел или нет, значит set, а не dict
  3. 3 Что есть каждый член? → составная идентичность (user, action), значит составной ключ
  4. 4 Этот составной объект хешируем? → используй tuple (неизменяемый, хешируемый), а не list
  5. 5 Итог: set из кортежей (user, action) — O(1) вхождение, автоматическая дедупликация
Вспомните перед уходом
  1. 01
    Почему tuple может быть ключом dict, а list — нет, и как это связано с изменяемостью?
  2. 02
    В чём ловушка изменяемого аргумента по умолчанию и какой правильный фикс?
Итог

Четыре базовых контейнера Python делятся по двум вопросам: изменяемость и хешируемость. list — твоя упорядоченная, изменяемая, растущая последовательность — JS Array, но in — это O(n)-скан, а .append(), а не .push(). dict — хеш-таблица с порядком вставки и O(1)-поиском, объединяющая JS Object и Map. set хранит уникальные хешируемые члены с O(1)-вхождением и настоящей алгеброй объединения/пересечения. tuple неизменяем и упорядочен, хешируем, когда хешируемы его элементы, — тип для фиксированных записей, множественного возврата и составных ключей dict/set, без аналога в JS. Изменяемость закрывает доступ к хешируемости: list не может быть ключом, tuple может. Повторяющиеся ошибки уровня сеньора — писать случайно квадратичный код, проверяя вхождение по list вместо set, опираться на is там, где нужен == (из-за кэша малых целых и строк в CPython is врёт), и ловушка изменяемого дефолта — лечится сентинелом None и свежим объектом на вызов. Теперь, когда видишь list в горячей проверке вхождения или попытку сделать list ключом словаря, — знаешь, на каком вопросе оно падает и к какому контейнеру тянуться вместо него.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.