Deep Engineering

MEASUREMENT

bench/collections/basics.py

The script that produced the numbers in the article, and the record of the run. The file is read from the repository at build time — this is the code that was run, not a copy of it.

Cited in
/en/interview/python/builtin-collections
How to run it
python3.11 bench/collections/basics.py
python3.12 bench/collections/basics.py
python3.13 bench/collections/basics.py
python3.14 bench/collections/basics.py

python3.13 bench/collections/cost.py
python3.14 bench/collections/cost.py

python3.11 bench/collections/hashable.py
python3.12 bench/collections/hashable.py
python3.13 bench/collections/hashable.py
python3.14 bench/collections/hashable.py

python3.13 bench/collections/build.py
python3.14 bench/collections/build.py

python3.11 bench/collections/practice.py
python3.12 bench/collections/practice.py
python3.13 bench/collections/practice.py
python3.14 bench/collections/practice.py

The run below is recorded in Russian. It is a lab record, kept in the language it was written in; the numbers, the tables and the code read the same either way.

Record of the run

Замеры для урока «Встроенные коллекции»

Скрипт Что делает
basics.py наблюдения без времени: дубли, хешируемость, порядок словаря и множества (множество строк при трёх разных PYTHONHASHSEED), размер контейнера на 1000 целых, сводка «что умеет каждый»
cost.py цена x in коллекция на 10, 1 000 и 100 000 элементов и удаления из начала: list.pop(0) против deque.popleft()
practice.py источник ответов двух практических задач урока
hashable.py полный короткий пример урока: значение словаря может быть списком, ключ и элемент множества — нет
build.py цена сценария «построить set из списка и проверить» против проверок по самому списку, 1 и 100 проверок
python3.11 bench/collections/basics.py
python3.12 bench/collections/basics.py
python3.13 bench/collections/basics.py
python3.14 bench/collections/basics.py

python3.13 bench/collections/cost.py
python3.14 bench/collections/cost.py

python3.11 bench/collections/hashable.py
python3.12 bench/collections/hashable.py
python3.13 bench/collections/hashable.py
python3.14 bench/collections/hashable.py

python3.13 bench/collections/build.py
python3.14 bench/collections/build.py

python3.11 bench/collections/practice.py
python3.12 bench/collections/practice.py
python3.13 bench/collections/practice.py
python3.14 bench/collections/practice.py

Записи в runs/: без суффикса — CPython 3.13.7, остальные — с суффиксом версии (3.11.15, 3.12.3, 3.14.7). Числа cost.py и build.py — этой машины (2 ядра; build.py, hashable.py и разделы 6 basics.py сняты 14.10.2026 на linux/x86_64). Отношения между формами — наблюдение этой конфигурации: они зависят от размера и вида данных, версии и машины, и на своей машине их надо мерить заново. CPython 3.15 (вышел 9 октября 2026) здесь не запускался.

Что получилось

Дубли. set оставляет разные элементы, dict.fromkeys — тоже, но в порядке первого появления; в литерале словаря с повторённым ключом побеждает последнее значение.

Хешируемость. Требование — к элементам множества и к ключам словаря, а не к значениям: {"items": []} создаётся, а {[]: "x"} и {[1, 2]} падают (hashable.py, на 3.11–3.14). Список и множество не могут быть ни элементом множества, ни ключом словаря; кортеж может, но только если хешируемо всё, что в нём: {(1, [2])} падает. frozenset — может. На 3.14 текст ошибки называет роль объекта: cannot use 'list' as a set element (…).

Порядок. Словарь хранит порядок вставки; удалённый и вставленный снова ключ уходит в конец. Множество из пяти строк при PYTHONHASHSEED = 1, 2, 3 напечаталось в трёх разных порядках — на всех четырёх версиях одинаково. Множество маленьких целых часто печатается по возрастанию, потому что hash(n) == n, но не всегда: {8, 1} печатается как {8, 1}.

Память на 1000 целых (сам контейнер, 3.13.7): list 8056 байт, tuple 8040, set 32984, dict 36952 — множество в 4,1 раза больше списка. На 3.14.7 кортеж — 8048.

x in коллекция, элемент последний (медианы, 3.13.7; в каждое число входит цикл — пустой стоит 18,7 нс на шаг): на 10 элементах список 119,5 нс против 35,2 у множества — но если элемент первый, список находит его за 33,7 нс. На 1 000 — 9 690 нс против 44,9 (в 216 раз), на 100 000 — около миллисекунды против 79,4 нс. У множества и ключей словаря время почти не зависит от размера.

Удаление из начала (50 000 удалений из 100 000, 3.13.7): list.pop(0) — 12 662 нс на удаление, deque.popleft() — 42,4, list.pop() с конца — 43,7.

Память и вид ключей (раздел 6, одинаково на 3.11–3.14): dict на 1000 ключей int — 36952 байта, на 1000 ключей str — 26032; set — 32984 в обоих случаях. Записи словаря со строковыми ключами в CPython хранятся без поля под хеш (PyDictUnicodeEntry в Include/internal/pycore_dict.h).

Построить множество ради проверки (build.py, медианы, 3.13.7, нс на сценарий): на 10 элементах одна проверка по списку — 98,8, set(lst) плюс одна проверка — 211,1; сто проверок — 8 499,6 против 1 962,7. На 1 000: одна проверка — 4 638,7 против 7 926,6, сто — 596 511,4 против 10 674,0. Ради одной проверки множество не окупилось, ради ста — окупилось.

Script

155 lines
"""Встроенные коллекции: что list, tuple, dict и set гарантируют и чего не гарантируют.

Времени здесь нет — только наблюдения. Цена поиска и удаления из начала — в
cost.py.

ШЕСТЬ СЮЖЕТОВ:

1. ДУБЛИ. Список хранит всё, множество — только разные элементы, у словаря
   ключи уникальны и при повторе побеждает последнее значение.

2. ХЕШИРУЕМОСТЬ. Ключом словаря и элементом множества может быть только
   хешируемый объект. Список — нет; кортеж — да, но только если хешируемо
   всё, что в нём лежит; frozenset — да. К ЗНАЧЕНИЯМ словаря требование не
   относится: {"items": []} работает — это показывает hashable.py.

3. ПОРЯДОК. Словарь хранит порядок вставки — это гарантия языка. Множество
   порядка не обещает: множество строк печатается в разном порядке при
   разных PYTHONHASHSEED. Скрипт запускает сам себя трижды с разными
   значениями переменной и сравнивает.

4. ПАМЯТЬ. Размер самого контейнера на 1000 целых — sys.getsizeof, без
   элементов: элементы общие и в счёт не идут.

5. ЧТО УМЕЕТ КАЖДЫЙ. Индекс, изменение на месте, ключ словаря — сводная
   проверка по четырём типам, которой и отвечают на «что когда брать».

6. ПАМЯТЬ И ВИД КЛЮЧЕЙ. dict на 1000 ключей-строк меньше, чем на 1000
   целых: в CPython записи словаря со строковыми ключами хранятся без поля
   под хеш. У множества размер от вида элементов не зависит.

Запускать: python3.11 / 3.12 / 3.13 / 3.14.
"""
import os
import subprocess
import sys

if os.environ.get("ORDER_CHILD") == "1":
    print(list({"яблоко", "груша", "слива", "вишня", "дыня"}))
    sys.exit(0)

print("PY", sys.version.split()[0])
print()


def err(fn):
    try:
        fn()
    except Exception as e:  # noqa: BLE001 — нам нужен текст
        return f"{type(e).__name__}: {e}"
    return "исключения нет"


# ── 1 ─────────────────────────────────────────────────────────────────────
print("1) дубли")
data = [3, 1, 3, 2, 1]
print("   list(data)          ->", list(data))
print("   set(data)           ->", set(data))
print("   dict.fromkeys(data) ->", dict.fromkeys(data))
print("   {'a': 1, 'a': 2}    ->", {"a": 1, "a": 2})
print("   len(set(data)) == len(data) ->", len(set(data)) == len(data))
print()

# ── 2 ─────────────────────────────────────────────────────────────────────
print("2) хешируемость: что можно класть в set и в ключи dict")
print("   {[1, 2]}            ->", err(lambda: {[1, 2]}))
print("   {[1, 2]: 'x'}       ->", err(lambda: {[1, 2]: "x"}))
print("   {(1, 2)}            ->", err(lambda: {(1, 2)}))
print("   {(1, [2])}          ->", err(lambda: {(1, [2])}))
print("   {frozenset({1, 2})} ->", err(lambda: {frozenset({1, 2})}))
print("   {{1, 2}}            ->", err(lambda: {{1, 2}}))
print()

# ── 3 ─────────────────────────────────────────────────────────────────────
print("3) порядок")
d = {}
for key in ["дыня", "яблоко", "груша"]:
    d[key] = len(key)
print("   dict в порядке вставки:", list(d))
d.pop("яблоко")
d["яблоко"] = 6
print("   удалили и вставили снова — ключ в конце:", list(d))
print("   множество строк при разных PYTHONHASHSEED:")
orders = set()
for seed in ["1", "2", "3"]:
    env = dict(os.environ, ORDER_CHILD="1", PYTHONHASHSEED=seed)
    out = subprocess.run(
        [sys.executable, __file__], env=env, capture_output=True, text=True, check=True
    ).stdout.strip()
    orders.add(out)
    print(f"   PYTHONHASHSEED={seed}: {out}")
print("   разных порядков из трёх запусков:", len(orders))
print("   множество целых {5, 1, 3}:", {5, 1, 3}, "— порядок от хеша, а не от вставки")
print("   hash(1), hash(3), hash(5):", hash(1), hash(3), hash(5), "— хеш небольшого целого равен ему самому")
print("   множество целых {8, 1}:", {8, 1}, "— и не всегда по возрастанию")
print()

# ── 4 ─────────────────────────────────────────────────────────────────────
print("4) память: сам контейнер на 1000 целых, байт (sys.getsizeof)")
items = list(range(1000))
sizes = {
    "list": sys.getsizeof(list(items)),
    "tuple": sys.getsizeof(tuple(items)),
    "set": sys.getsizeof(set(items)),
    "dict": sys.getsizeof(dict.fromkeys(items)),
}
for name, size in sizes.items():
    print(f"   {name:<6}{size:>7}")
print(f"   set / list: {sizes['set'] / sizes['list']:.1f}")
print()

# ── 5 ─────────────────────────────────────────────────────────────────────
print("5) что умеет каждый")
samples = {"list": [1, 2], "tuple": (1, 2), "set": {1, 2}, "dict": {1: "a", 2: "b"}}


def can_index(x):
    try:
        x[0]
        return "да"
    except (TypeError, KeyError):
        return "нет"


def can_change(x):
    return "да" if hasattr(x, "append") or hasattr(x, "add") or hasattr(x, "__setitem__") else "нет"


def can_be_key(x):
    try:
        hash(x)
        return "да"
    except TypeError:
        return "нет"


print(f"   {'тип':<6}{'x[0]':>6}{'менять':>8}{'ключ':>6}")
for name, x in samples.items():
    idx = can_index(x) if name != "dict" else "по ключу"
    print(f"   {name:<6}{idx:>6}{can_change(x):>8}{can_be_key(x):>6}")
print()

# ── 6 ─────────────────────────────────────────────────────────────────────
# Добавлено 14.10.2026 по внешнему аудиту (PY-COL-02). Раздел 4 сравнивает
# контейнеры на целых. У словаря размер зависит и от вида ключей: записи
# словаря с ключами-строками (str) в CPython 3.11–3.14 хранятся без поля
# под хеш (Include/internal/pycore_dict.h: PyDictUnicodeEntry против
# PyDictKeyEntry), у множества запись одна и та же.
print("6) память и вид ключей: 1000 ключей, байт (sys.getsizeof)")
ints = list(range(1000))
strs = [str(i) for i in ints]
print(f"   dict, ключи int   {sys.getsizeof(dict.fromkeys(ints)):>7}")
print(f"   dict, ключи str   {sys.getsizeof(dict.fromkeys(strs)):>7}")
print(f"   set, int          {sys.getsizeof(set(ints)):>7}")
print(f"   set, str          {sys.getsizeof(set(strs)):>7}")