Встроенные коллекции: list, tuple, dict и set — что когда брать
Вопрос «что выбрать — список, кортеж, словарь или множество» звучит как вопрос вкуса, а решается тремя проверками: нужен ли порядок, нужны ли повторы и как вы будете искать. Прогон отвечает числами: проверка x in по списку из тысячи элементов медленнее, чем по множеству, в 216 раз, а на десяти — всего в 3,4 (CPython 3.13.7, одна машина); словарь хранит порядок вставки, а множество строк печатается по-разному при разных PYTHONHASHSEED; и множество на тысячу чисел в 4,1 раза больше списка.
Полное техническое изложение
TL;DR
В Python четыре основные встроенные коллекции, и выбирают между ними по трём вопросам: нужен ли порядок, нужны ли повторы и как вы будете искать. Список — упорядоченный и изменяемый, с повторами. Кортеж — то же, но неизменяемый. Словарь — ключи и значения, ключи уникальны. Множество — уникальные элементы без порядка.
Отсюда главное следствие: если проверок «есть ли такой элемент» много,
делать их стоит по множеству или словарю, а не по списку. Список проверяет
элементы по очереди, множество находит элемент по хешу — числу, которое
объект вычисляет из своего значения (hash(x)). В среднем это O(1): время
почти не зависит от размера; в худшем случае, когда хеши совпадают, — O(n)
(подробно — в уроке «Сложность операций»). В нашем прогоне (CPython 3.13.7,
одна машина) на тысяче элементов x in по списку было медленнее, чем по
множеству, в 216 раз, на ста тысячах — в тринадцать тысяч; на своей машине
отношение надо мерить. Плата за это — память и хешируемость: множество на
тысячу чисел в 4,1 раза больше списка, а элементы множества и ключи словаря
должны быть хешируемыми — список ими быть не может. Значения словаря —
могут: {"items": []} работает.
Дальше — то, что отличает понимание от пересказа. На десяти элементах
разница всего в 3,4 раза, а первый элемент список находит так же быстро, как
множество. Строить множество из списка ради одной проверки в замере не
окупилось — это дольше одного перебора. Словарь с Python 3.7 хранит порядок вставки — это гарантия языка,
а множество порядка не обещает: множество из пяти строк при трёх разных
PYTHONHASHSEED напечаталось в трёх разных порядках. А очередь на списке —
ошибка: на ста тысячах элементов list.pop(0) в 299 раз дороже
deque.popleft().
- как создать список и словарь и пройти по ним циклом
for; - что такое изменяемый и неизменяемый объект;
- что выражение
x in yпроверяет вхождение; - что такое класс и экземпляр — урок «Классы и ООП»: он объясняет, почему экземпляры обычных классов хешируемы;
- запись O(1) и O(n) — хотя бы как «не растёт» и «растёт вместе с размером»; подробно — в уроке «Сложность операций».
- хешируемость,
frozenset,dict.fromkeys; - порядок словаря и множества,
PYTHONHASHSEED; - цена
x in,list.pop(0)иcollections.deque.
База: четыре коллекции и три вопроса
| коллекция | порядок | повторы | изменяемая | как ищут |
|---|---|---|---|---|
list | да | да | да | по индексу; по значению — перебором |
tuple | да | да | нет | по индексу; по значению — перебором |
dict | порядок вставки | ключи уникальны | да | по ключу, через хеш |
set | нет | нет | да | по значению, через хеш |
Ответ уровня Junior на «что когда брать»: список — для упорядоченного набора, который меняется; кортеж — для неизменяемой записи из нескольких полей; словарь — когда значение ищут по ключу; множество — когда нужны уникальные элементы и быстрая проверка «есть ли такой». Это верно, и за этим ответом обычно идут три уточнения: насколько быстрее проверка по множеству, почему список нельзя положить в множество и что с порядком. Им и посвящены механизмы ниже.
Чем список отличается от кортежа в памяти и по скорости — отдельный урок, «Список против кортежа». Здесь — выбор между всеми четырьмя.
Механизм 1: повторы и уникальность
Вывод bench/collections/basics.py, раздел 1:
1) дубли
list(data) -> [3, 1, 3, 2, 1]
set(data) -> {1, 2, 3}
dict.fromkeys(data) -> {3: None, 1: None, 2: None}
{'a': 1, 'a': 2} -> {'a': 2}
len(set(data)) == len(data) -> False
Список хранит всё, что в него положили. Множество — только разные элементы, и порядок исходного списка теряется. Словарь хранит уникальные ключи, и при повторе ключа побеждает последнее значение.
Из этого вывода складываются два рабочих приёма:
- «Есть ли дубли» —
len(set(data)) != len(data). - «Убрать дубли, сохранив порядок» —
list(dict.fromkeys(data)). Черезsetтак нельзя: у множества порядка нет (Механизм 3), а у словаря он есть.
Механизм 2: хешируемость — кто может быть ключом
Элементы множества и ключи словаря должны быть хешируемыми. К значениям словаря это требование не относится. Множество и словарь находят элемент или ключ по хешу, поэтому хеш у него должен быть и не должен меняться; значение словаря лежит рядом с ключом, по нему не ищут, и хешировать его незачем:
Values that are not hashable, that is, values containing lists, dictionaries
or other mutable types (that are compared by value rather than by object
identity) may not be used as keys.
Нехешируемые значения — то есть содержащие списки, словари или другие изменяемые типы, которые сравниваются по значению, а не по тождеству, — ключами быть не могут.
Это видно на полном коротком примере. Полный файл
bench/collections/hashable.py:
import sys
print("PY", sys.version.split()[0])
d = {"items": []}
d["items"].append(1)
print('{"items": []} after append:', d)
print('{(1, 2): "ok"}:', {(1, 2): "ok"})
for text, make in [('{[]: "x"}', lambda: {[]: "x"}), ("{[1, 2]}", lambda: {[1, 2]})]:
try:
make()
except TypeError as e:
print(text, "-> TypeError:", e)Запуск — python3.13 bench/collections/hashable.py; вывод на CPython 3.13.7
(на 3.11 и 3.12 — тот же):
PY 3.13.7
{"items": []} after append: {'items': [1]}
{(1, 2): "ok"}: {(1, 2): 'ok'}
{[]: "x"} -> TypeError: unhashable type: 'list'
{[1, 2]} -> TypeError: unhashable type: 'list'
Словарь со списком в значении создался и даже изменился на месте; кортеж
стал ключом. Список не годится ни в ключ, ни в элемент множества. Подробнее
о том, что хешируемо, — вывод bench/collections/basics.py, раздел 2:
2) хешируемость: что можно класть в set и в ключи dict
{[1, 2]} -> TypeError: unhashable type: 'list'
{[1, 2]: 'x'} -> TypeError: unhashable type: 'list'
{(1, 2)} -> исключения нет
{(1, [2])} -> TypeError: unhashable type: 'list'
{frozenset({1, 2})} -> исключения нет
{{1, 2}} -> TypeError: unhashable type: 'set'
Строка, которую стоит запомнить, — {(1, [2])}. Кортеж хешируем, только
если хешируемо всё, что в нём лежит: хеш кортежа считается из хешей
элементов, а у списка его нет. Поэтому ключом «из нескольких полей» служит
кортеж неизменяемых значений, а множество множеств строят из frozenset.
Почему хеша нет у изменяемых объектов, которые сравниваются по значению, —
списков, словарей, множеств: словарь кладёт ключ по хешу и потом ищет его по
тому же хешу. Изменись такой ключ после вставки — его хеш изменился бы, и
словарь искал бы не там. Со значением такой беды нет: его не ищут по хешу,
поэтому d["items"].append(1) выше ничего не ломает. Экземпляры обычных
классов изменяемы, но хешируемы: они сравниваются по тождеству, а не по
значению, — об этом урок «Классы и ООП». Как это устроено внутри, разобрано
в статье о словарях.
Механизм 3: порядок — у словаря есть, у множества нет
Dictionaries preserve insertion order. Note that updating a key does not
affect the order. Keys added after deletion are inserted at the end.
Словари хранят порядок вставки. Обновление значения по ключу порядок не меняет. Ключи, добавленные после удаления, вставляются в конец.
У множества такой гарантии нет. Порядок, в котором оно печатается, зависит от хешей элементов, а хеш строки от запуска к запуску разный:
By default, the __hash__() values of str and bytes objects are "salted" with
an unpredictable random value. Although they remain constant within an
individual Python process, they are not predictable between repeated
invocations of Python.
По умолчанию значения __hash__() у объектов str и bytes „подсаливаются“ непредсказуемым случайным значением. В пределах одного процесса Python они постоянны, но между повторными запусками Python непредсказуемы.
Это случайное «зерно» можно зафиксировать переменной окружения
PYTHONHASHSEED. Скрипт запустил сам себя трижды с разными значениями —
вывод bench/collections/basics.py, раздел 3:
3) порядок
dict в порядке вставки: ['дыня', 'яблоко', 'груша']
удалили и вставили снова — ключ в конце: ['дыня', 'груша', 'яблоко']
множество строк при разных PYTHONHASHSEED:
PYTHONHASHSEED=1: ['дыня', 'груша', 'слива', 'яблоко', 'вишня']
PYTHONHASHSEED=2: ['вишня', 'яблоко', 'дыня', 'слива', 'груша']
PYTHONHASHSEED=3: ['дыня', 'слива', 'яблоко', 'груша', 'вишня']
разных порядков из трёх запусков: 3
множество целых {5, 1, 3}: {1, 3, 5} — порядок от хеша, а не от вставки
hash(1), hash(3), hash(5): 1 3 5 — хеш небольшого целого равен ему самому
множество целых {8, 1}: {8, 1} — и не всегда по возрастанию
Последние строки — ловушка наоборот. Множество маленьких целых часто
печатается «по возрастанию», потому что хеш небольшого целого равен ему
самому. Но это совпадение реализации, а не сортировка: {8, 1} напечаталось
как {8, 1}, а код, который опирается на порядок, сломается на строках. Если нужен порядок — список, словарь или
sorted(...).
Механизм 4: цена поиска — где разница огромна, а где её нет
Документация называет «быструю проверку вхождения» первым применением множеств. Насколько быструю — замер. Искомый элемент последний, то есть для списка худший случай:
| размер | list | set | dict (ключи) | list / set |
|---|---|---|---|---|
| 10 | 119,5 нс | 35,2 нс | 35,4 нс | 3,4 |
| 1 000 | 9 690,0 нс | 44,9 нс | 48,4 нс | 216,0 |
| 100 000 | 1 052 432,4 нс | 79,4 нс | 56,7 нс | 13 250,6 |
Список проверяет элементы по очереди, поэтому его время растёт вместе с размером. Множество и словарь вычисляют хеш и идут в нужное место таблицы; когда хеши распределены хорошо, как у этих целых, проверить приходится немного ячеек, — поэтому их время растёт в разы, а не в тысячи раз, как у списка. Кортеж ведёт себя как список: это тоже последовательность.
Но O(1) у множества и словаря — средняя оценка, а не обещание одной
проверки. Если у многих ключей один и тот же хеш, поиск сравнивает их
по очереди, и худший случай — O(n): в уроке «Сложность операций» поиск в
множестве из ста объектов с одинаковым хешем вызвал __eq__ 105 раз, а не
один. И одна проверка сама не бесплатна: она стоит вычисления хеша и вызова
__eq__, а у длинной строки и то и другое проходит по её символам.
В числах есть и сам цикл for, в котором шла проверка: пустой цикл стоит
18,7 нс на шаг — строка «пустой цикл» в записи прогона. На больших размерах
это мелочь, а на десяти элементах — половина времени множества, поэтому
3,4 раза — оценка снизу: сама проверка отличается сильнее.
Но правило «всегда множество» на маленьких коллекциях не работает. На десяти
элементах разница всего в 3,4 раза, а если искомый элемент первый, список
находит его за 33,7 нс — так же быстро, как множество. И строить множество
ради одной проверки невыгодно: построение хеширует каждый элемент и
раскладывает его по таблице. Замер выше этого не видит — там коллекции
готовы до начала отсчёта, — поэтому построение мерили отдельно. Вывод
bench/collections/build.py (CPython 3.13.7):
построить set ради проверок: нс на сценарий, медианы 9 кругов
размер проверок по списку set(lst) + проверки
10 1 98.8 211.1
10 100 8499.6 1962.7
1000 1 4638.7 7926.6
1000 100 596511.4 10674.0
Ради одной проверки построение множества проиграло перебору и на десяти, и на тысяче элементов; ради ста — выиграло. Множество окупается, когда проверок много; если же оно уже есть, оно быстрее и на десяти элементах. Где точка окупаемости на ваших данных — вопрос замера, а не правила.
Механизм 5: очередь — не список
Вывод bench/collections/cost.py, раздел 2:
2) удаление из начала: 50 000 удалений из коллекции в 100 000, нс на удаление
(создание коллекции в замер не входит)
форма мин медиана макс
list.pop(0) 11920.9 12662.3 13358.6
deque.popleft() 41.6 42.4 47.3
list.pop() с конца 42.5 43.7 46.3
list.pop(0) / deque.popleft() по медианам: 299
Список хранит элементы подряд, и удаление первого сдвигает все остальные на
одну позицию — чем длиннее список, тем дороже. При удалении с конца сдвигать
нечего, поэтому list.pop() дешёвый. Для очереди «добавляем в конец,
забираем из начала» в стандартной библиотеке есть collections.deque —
a list-like sequence optimized for data accesses near its endpoints
(похожая на список последовательность, оптимизированная для доступа к данным у её концов). В
замере popleft() дешевле list.pop(0) в 299 раз.
Глубже: что стоит память
Вывод bench/collections/basics.py, раздел 4:
4) память: сам контейнер на 1000 целых, байт (sys.getsizeof)
list 8056
tuple 8040
set 32984
dict 36952
set / list: 4.1
sys.getsizeof считает сам контейнер, без элементов: числа общие и в счёт не
идут. Список и кортеж хранят по указателю на элемент — 8 байт на 64-битной
сборке, плюс заголовок. Множество и словарь держат хеш-таблицу с запасом
пустых ячеек — без запаса поиск по хешу перестал бы быть быстрым, — плюс
служебные данные. Отсюда в 4,1 раза больше памяти у множества: это цена
быстрого in. Но 4,1 — отношение для тысячи целых на этой версии, а не
константа: оно зависит от числа и вида элементов и от реализации.
Устроены множество и словарь в CPython по-разному. У множества каждая
ячейка таблицы — указатель на элемент и его запомненный хеш (setentry,
Include/cpython/setobject.h:20–23 в CPython 3.13.7). Словарь — это таблица
индексов (dk_indices) плюс отдельный массив записей (dk_entries). Запись общего
вида хранит хеш, ключ и значение (PyDictKeyEntry,
Include/internal/pycore_dict.h:76–81), а запись словаря, у которого все
ключи — строки, только ключ и значение (PyDictUnicodeEntry, строки 83–86):
хеш строка хранит в самой себе (поле hash,
Include/cpython/unicodeobject.h:101). Это видно по памяти — вывод
bench/collections/basics.py, раздел 6, одинаковый на 3.11–3.14:
6) память и вид ключей: 1000 ключей, байт (sys.getsizeof)
dict, ключи int 36952
dict, ключи str 26032
set, int 32984
set, str 32984
Словарь на тех же тысяче ключах, но строковых, меньше на 10 920 байт; у множества размер от вида элементов не зависит. Детали — в статьях об устройстве списка и словаря.
Как отвечать на собеседовании
Короткий ответ: список — упорядоченный изменяемый набор с повторами, кортеж — неизменяемый, словарь — ключ → значение с уникальными ключами, множество — уникальные элементы без порядка. Проверку вхождения и поиск по значению делают по множеству или словарю: там это поиск по хешу — O(1) в среднем и O(n) в худшем случае, когда хеши совпадают, — а в списке — перебор. Элементом множества и ключом словаря может быть только хешируемый объект; значения словаря могут быть любыми. Словарь хранит порядок вставки, множество — нет.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
На «насколько быстрее множество». «Зависит от размера и от данных. В
моём прогоне на CPython 3.13.7 x in по списку на тысяче элементов был
медленнее в 216 раз, на десяти — в 3,4, а первый элемент список находит так
же быстро. Строить множество ради одной проверки не стоит: в замере это
дольше одного перебора, окупилось на ста проверках. O(1) у множества —
среднее: при совпадающих хешах поиск вырождается в перебор. Точные
отношения на другой машине — мерить».
На «почему список нельзя положить в множество». «Множество ищет по хешу,
а хеш изменяемого объекта мог бы поменяться после вставки. Поэтому у списка
хеша нет. Кортеж хешируем, только если хешируемо всё, что в нём:
{(1, [2])} падает. А значением словаря список быть может — по значениям не
ищут: {"items": []} работает».
На «упорядочен ли словарь». «С 3.7 порядок вставки — гарантия языка.
Удалённый и вставленный снова ключ уходит в конец. У множества порядка нет:
множество строк при разных PYTHONHASHSEED печаталось по-разному».
На «как сделать очередь». «collections.deque. list.pop(0) сдвигает все
элементы — у меня он был в 299 раз дороже popleft() на ста тысячах».
Дальше спросят
Как посчитать, сколько раз встречается каждый элемент?
Словарём: counts[x] = counts.get(x, 0) + 1. Или готовым
collections.Counter(data) — это словарь, у которого отсутствующий ключ даёт
ноль, плюс методы вроде most_common(). Для группировки похожая заготовка —
collections.defaultdict(list).
Что такое frozenset и зачем он?
Неизменяемое множество. Раз оно не меняется, у него есть хеш — и его можно
положить в другое множество или сделать ключом словаря. Обычное множество
для этого не годится: {{1, 2}} падает с unhashable type: 'set'.
Чем словарь отличается от списка пар?
Поиском. В списке пар [(ключ, значение), …] найти значение по ключу — это
перебор, как x in list. Словарь находит ключ по хешу — в среднем за время,
почти не зависящее от размера, — и гарантирует уникальность ключей. Список пар уместен,
когда ключи повторяются или нехешируемы.
Частые заблуждения
Проверку вхождения всегда надо делать по множеству
Если множество уже есть и проверок много — да. Но на десяти элементах разница всего в 3,4 раза, а первый элемент список находит за 33,7 нс, как множество. Строить множество ради одной проверки — это хешировать каждый элемент: в замере на CPython 3.13.7 это вышло дольше одного перебора и на десяти, и на тысяче элементов.
Всё, что лежит в словаре, должно быть хешируемым
Только ключи — и элементы множества. Значение может быть списком или другим словарём: {"items": []} создаётся и меняется на месте, а {[]: "x"} падает с unhashable type: 'list'.
Поиск по множеству — всегда одна проверка
O(1) — средняя оценка при хорошо распределённых хешах. Если хеши совпадают, множество сравнивает элементы по очереди: в уроке «Сложность операций» поиск среди ста объектов с одним хешем вызвал __eq__ 105 раз. Худший случай — O(n).
Множество сортирует элементы
Множество маленьких целых часто, но не всегда печатается по возрастанию — хеш такого числа равен ему самому; {8, 1} напечаталось как {8, 1}. Множество строк при разных PYTHONHASHSEED напечаталось в трёх разных порядках.
Словарь не хранит порядок
С Python 3.7 хранит порядок вставки — это гарантия языка, а не деталь реализации. Удалённый и вставленный снова ключ уходит в конец.
Кортеж всегда можно сделать ключом словаря
Только если хешируемо всё, что в нём лежит. {(1, [2])} падает с unhashable type: 'list'.
Очередь удобно делать на списке через pop(0)
Удаление первого элемента сдвигает все остальные: на ста тысячах list.pop(0) оказался в 299 раз дороже deque.popleft().
История версий
| Версия | Изменение | Что это значит для кода |
|---|---|---|
| 3.7 | Порядок вставки словаря становится гарантией языка. В CPython 3.6 это была деталь реализации, полагаться на которую было нельзя, а раньше порядка не было вовсе. | |
| 3.11 | Опорная точка урока. Наблюдения скрипта на 3.11, 3.12 и 3.13 совпадают построчно, включая порядки множества при одинаковом PYTHONHASHSEED. Записи словаря со строковыми ключами без поля под хеш (PyDictUnicodeEntry) есть во всех четырёх проверенных версиях. | |
| 3.14 | Текст ошибки о нехешируемом объекте называет роль: cannot use 'list' as a set element (unhashable type: 'list') и cannot use 'list' as a dict key (…); для {(1, [2])} — cannot use 'tuple' as a set element (unhashable type: 'list'). Кортеж на 1000 элементов стал на 8 байт больше — 8048 против 8040. Поведение коллекций не изменилось. | |
| 3.15 | Новый встроенный неизменяемый тип frozendict (PEP 814). Урок его не запускал: Python 3.15 вышел 9 октября 2026, наши прогоны — 3.11–3.14. По документу What’s New In Python 3.15 (сверено 14.10.2026): A frozendict is hashable as long as all of its keys and values are hashable (frozendict хешируем, если хешируемы все его ключи и значения) — в отличие от обычного словаря, где хешируемыми обязаны быть только ключи. |
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
words = ["b", "a", "b", "c", "a"] print(list(dict.fromkeys(words))) print(sorted(set(words))) print(len(set(words)), len(words))
Практика · оцените
Проверьте себя
Нужно хранить уникальные id пользователей и часто проверять, есть ли id в наборе. Что подходит лучше всего?
Чем измерено
Числа этого урока получены этими скриптами. Каждый открывается прямо отсюда — вместе с записью прогона: на чём считали, что получилось и с каким разбросом.
Наблюдения без времени — CPython 3.11.15, 3.12.3, 3.13.7 и 3.14.7; замеры
времени — 3.13.7 и 3.14.7 на одной машине с двумя ядрами. Скрипты
bench/collections/hashable.py, bench/collections/build.py и раздел 6
bench/collections/basics.py добавлены и запущены 14.10.2026 на linux/x86_64.
Контракты и цитаты сверены 14.10.2026. Python 3.15 (вышел 9 октября 2026)
урок не запускал — о нём только строка в истории версий.
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- В Python четыре основные встроенные коллекции, и выбирают между ними по трём вопросам: нужен ли порядок, нужны ли повторы и как вы будете искать. Список — упорядоченный и изменяемый, с повторами. Кортеж — то же, но неизменяемый. Словарь — ключи и значения, ключи уникальны. Множество — уникальные элементы без порядка.
- Отсюда главное следствие: если проверок «есть ли такой элемент» много, делать их стоит по множеству или словарю, а не по списку. Список проверяет элементы по очереди, множество находит элемент по хешу — числу, которое объект вычисляет из своего значения (
hash(x)). В среднем это O(1): время почти не зависит от размера; в худшем случае, когда хеши совпадают, — O(n) (подробно — в уроке «Сложность операций»). В нашем прогоне (CPython 3.13.7, одна машина) на тысяче элементовx inпо списку было медленнее, чем по множеству, в 216 раз, на ста тысячах — в тринадцать тысяч; на своей машине отношение надо мерить. Плата за это — память и хешируемость: множество на тысячу чисел в 4,1 раза больше списка, а элементы множества и ключи словаря должны быть хешируемыми — список ими быть не может. Значения словаря — могут:{"items": []}работает. - Дальше — то, что отличает понимание от пересказа. На десяти элементах разница всего в 3,4 раза, а первый элемент список находит так же быстро, как множество. Строить множество из списка ради одной проверки в замере не окупилось — это дольше одного перебора. Словарь с Python 3.7 хранит порядок вставки — это гарантия языка, а множество порядка не обещает: множество из пяти строк при трёх разных
PYTHONHASHSEEDнапечаталось в трёх разных порядках. А очередь на списке — ошибка: на ста тысячах элементовlist.pop(0)в 299 раз дорожеdeque.popleft().
На самом деле
- Если множество уже есть и проверок много — да. Но на десяти элементах разница всего в 3,4 раза, а первый элемент список находит за 33,7 нс, как множество. Строить множество ради одной проверки — это хешировать каждый элемент: в замере на CPython 3.13.7 это вышло дольше одного перебора и на десяти, и на тысяче элементов.
- Только ключи — и элементы множества. Значение может быть списком или другим словарём:
{"items": []}создаётся и меняется на месте, а{[]: "x"}падает сunhashable type: 'list'. - O(1) — средняя оценка при хорошо распределённых хешах. Если хеши совпадают, множество сравнивает элементы по очереди: в уроке «Сложность операций» поиск среди ста объектов с одним хешем вызвал
__eq__105 раз. Худший случай — O(n). - Множество маленьких целых часто, но не всегда печатается по возрастанию — хеш такого числа равен ему самому;
{8, 1}напечаталось как{8, 1}. Множество строк при разныхPYTHONHASHSEEDнапечаталось в трёх разных порядках. - С Python 3.7 хранит порядок вставки — это гарантия языка, а не деталь реализации. Удалённый и вставленный снова ключ уходит в конец.
- Только если хешируемо всё, что в нём лежит.
{(1, [2])}падает сunhashable type: 'list'. - Удаление первого элемента сдвигает все остальные: на ста тысячах
list.pop(0)оказался в 299 раз дорожеdeque.popleft().
По версиям
- 3.7
- Порядок вставки словаря становится гарантией языка. В CPython 3.6 это была деталь реализации, полагаться на которую было нельзя, а раньше порядка не было вовсе.<
- 3.11
- Опорная точка урока. Наблюдения скрипта на 3.11, 3.12 и 3.13 совпадают построчно, включая порядки множества при одинаковом
PYTHONHASHSEED. Записи словаря со строковыми ключами без поля под хеш (PyDictUnicodeEntry) есть во всех четырёх проверенных версиях.< - 3.14
- Текст ошибки о нехешируемом объекте называет роль:
cannot use 'list' as a set element (unhashable type: 'list')иcannot use 'list' as a dict key (…); для{(1, [2])}—cannot use 'tuple' as a set element (unhashable type: 'list'). Кортеж на 1000 элементов стал на 8 байт больше — 8048 против 8040. Поведение коллекций не изменилось.< - 3.15
- Новый встроенный неизменяемый тип
frozendict(PEP 814). Урок его не запускал: Python 3.15 вышел 9 октября 2026, наши прогоны — 3.11–3.14. По документу What’s New In Python 3.15 (сверено 14.10.2026): A frozendict is hashable as long as all of its keys and values are hashable — в отличие от обычного словаря, где хешируемыми обязаны быть только ключи.<
Что разобрано
- База: четыре коллекции и три вопроса
- Механизм 1: повторы и уникальность
- Механизм 2: хешируемость — кто может быть ключом
- Механизм 3: порядок — у словаря есть, у множества нет
- Механизм 4: цена поиска — где разница огромна, а где её нет
- Механизм 5: очередь — не список
- Глубже: что стоит память
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- История версий
- Практика
- Проверьте себя
- Чем измерено
Источники и что читать дальше
6 ИСТОЧНИКОВ
- Модель данных — множестваОфициальная документация. Что такое множество и зачем оно: «These represent unordered, finite sets of unique, immutable objects» (Это неупорядоченные конечные множества уникальных неизменяемых объектов). И прямо названные применения: «Common uses for sets are fast membership testing, removing duplicates from a sequence, and computing mathematical operations such as intersection, union, difference, and symmetric difference» (Обычные применения множеств — быстрая проверка вхождения, удаление дублей из последовательности и математические операции: пересечение, объединение, разность и симметрическая разность). Текст сверен по встроенной справке pydoc (модуль pydoc_data.topics) CPython 3.13.7 и 3.14.7.https://docs.python.org/3.13/reference/datamodel.html#the-standard-type-hierarchy
- Встроенные типы — словариОфициальная документация. Гарантия порядка: «Dictionaries preserve insertion order. Note that updating a key does not affect the order. Keys added after deletion are inserted at the end» (Словари хранят порядок вставки. Обновление значения по ключу порядок не меняет. Ключи, добавленные после удаления, вставляются в конец). И с какой версии: «Changed in version 3.7: Dictionary order is guaranteed to be insertion order. This behavior was an implementation detail of CPython from 3.6» (Изменено в версии 3.7: порядок словаря гарантированно совпадает с порядком вставки. Начиная с CPython 3.6 это было деталью реализации). И какие ключи допустимы: «Values that are not hashable, that is, values containing lists, dictionaries or other mutable types (that are compared by value rather than by object identity) may not be used as keys» (Нехешируемые значения — то есть содержащие списки, словари или другие изменяемые типы, которые сравниваются по значению, а не по тождеству, — ключами быть не могут). Сверено по pydoc_data.topics 3.13.7 и 3.14.7.https://docs.python.org/3.13/library/stdtypes.html#mapping-types-dict
- Модель данных — __hash__Официальная документация. Почему порядок множества строк меняется от запуска к запуску: «By default, the __hash__() values of str and bytes objects are “salted” with an unpredictable random value. Although they remain constant within an individual Python process, they are not predictable between repeated invocations of Python» (По умолчанию значения __hash__() у объектов str и bytes „подсаливаются“ непредсказуемым случайным значением. В пределах одного процесса Python они постоянны, но между повторными запусками Python непредсказуемы). И что это значит для множеств: «Changing hash values affects the iteration order of sets. Python has never made guarantees about this ordering» (Изменение значений хеша влияет на порядок обхода множеств. Python никогда не давал гарантий относительно этого порядка). Сверено по pydoc_data.topics 3.13.7 и 3.14.7.https://docs.python.org/3.13/reference/datamodel.html#object.__hash__
- collections.dequeОфициальная документация. Строка документации типа в интерпретаторе: «A list-like sequence optimized for data accesses near its endpoints» (Похожая на список последовательность, оптимизированная для доступа к данным у её концов). Сверено вызовом collections.deque.__doc__ на 3.13.7 и 3.14.7.https://docs.python.org/3.13/library/collections.html#collections.deque
- Include/internal/pycore_dict.h и Include/cpython/setobject.hИсходный код CPython. Как устроены записи таблиц в CPython 3.13.7 (сверено по заголовкам, поставляемым с интерпретатором, 14.10.2026). Запись словаря общего вида — pycore_dict.h:76–81, PyDictKeyEntry: «/* Cached hash code of me_key. */» (запомненный хеш ключа) и поля me_hash, me_key, me_value. Запись словаря со строковыми ключами — строки 83–86, PyDictUnicodeEntry: только me_key и me_value, «/* The key must be Unicode and have hash. */» (ключ обязан быть строкой и иметь хеш). Сама хеш-таблица словаря — массив индексов dk_indices, строки 167–180: «Actual hash table of dk_size entries. It holds indices in dk_entries» (Собственно хеш-таблица из dk_size ячеек. Она хранит индексы в dk_entries). Запись множества — setobject.h:20–23, setentry: key и hash, «/* Cached hash code of the key */» (запомненный хеш ключа). На 3.14.7 те же структуры — pycore_dict.h:80–90; PyDictUnicodeEntry есть и в заголовках 3.11 и 3.12.https://github.com/python/cpython/blob/v3.13.7/Include/internal/pycore_dict.h
- What’s New In Python 3.15 — frozendictОфициальная документация. PEP 814: «A new immutable type, frozendict, is added to the builtins module. … A frozendict is hashable as long as all of its keys and values are hashable» (В модуль builtins добавлен новый неизменяемый тип frozendict. … frozendict хешируем, если хешируемы все его ключи и значения). Python 3.15 вышел 9 октября 2026; урок его не запускал — цитата сверена 14.10.2026.https://docs.python.org/3.15/whatsnew/3.15.html