Списки, словари, множества, кортежи
В Python четыре базовых контейнера — list, dict, set, tuple. Выбор правильного — это вопрос изменяемости и хешируемости, а ошибка стоит O(n) там, где O(1) был бесплатным.
Ревьюер помечает твою функцию: на каждый запрос она сканирует 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— упорядоченный, изменяемый, рабочая лошадка-последовательность. Ближе всего к JSArray, но методы другие (.append(), а не.push();.sort()меняет на месте и возвращаетNone). Добавление в конец — амортизированный O(1);insert(0, x),del lst[0]иx in lst— все O(n). Не хешируем.dict— хеш-таблица с гарантированным порядком вставки начиная с Python 3.7. Ключи должны быть хешируемыми; поиск, вставка и удаление — O(1) в среднем. Это объединённые в один тип JSObjectиMap: и со строковыми ключами, и с произвольными хешируемыми ключами, упорядоченный, быстрый.set— неупорядоченная коллекция уникальных хешируемых членов. O(1) на вхождение, плюс первоклассные|объединение,&пересечение,-разность. Как JSSet, но с настоящей алгеброй множеств из коробки.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 257 — False: деталь реализации, на которую нельзя полагаться. Правило: == для проверок значений, 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 Нужно быстрое вхождение / уникальность? → да, значит хеш-контейнер (set/dict), а не list
- 2 Это чистое вхождение (видел или нет) или я храню значение? → видел или нет, значит set, а не dict
- 3 Что есть каждый член? → составная идентичность (user, action), значит составной ключ
- 4 Этот составной объект хешируем? → используй tuple (неизменяемый, хешируемый), а не list
- 5 Итог: set из кортежей (user, action) — O(1) вхождение, автоматическая дедупликация
- 01Почему tuple может быть ключом dict, а list — нет, и как это связано с изменяемостью?
- 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-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.