Skip to content

Latest commit

 

History

History
154 lines (114 loc) · 7.6 KB

File metadata and controls

154 lines (114 loc) · 7.6 KB

Модуль collections: defaultdict, Counter, deque, namedtuple

← Модуль itertools · 🏠 Домой · Сортировка →


Чем defaultdict отличается от обычного словаря?

Коротко. При обращении к отсутствующему ключу он не бросает KeyError, а создаёт значение вызовом фабрики, переданной в конструктор, и сохраняет его в словаре.

from collections import defaultdict

groups = defaultdict(list)
groups["a"].append(1)      # ключа не было — создался пустой список
print(dict(groups))
# {'a': [1]}

Фабрика — любой вызываемый объект без аргументов: list, set, int (даёт 0 — удобно для счётчиков), lambda: "n/a".

Подвох. Чтение тоже создаёт ключ — это главный источник сюрпризов:

counts = defaultdict(int)
if counts["missing"] > 0:   # просто посмотрели
    ...
print(dict(counts))
# {'missing': 0}  — ключ теперь есть

Если побочный эффект не нужен, берут dict.get(key, default) или dict.setdefault(key, []). Разница между ними: setdefault вычисляет дефолтное значение всегда, даже когда ключ есть, а defaultdict — только при промахе.

Глубже. Механика — метод __missing__, который dict.__getitem__ вызывает при отсутствии ключа. Его можно определить и в своём подклассе dict; defaultdict — просто готовая реализация. Обратите внимание, что __missing__ не участвует в get() и в in.


Что умеет Counter?

Коротко. Это подкласс dict, считающий вхождения: ключ → количество. Отсутствующий ключ даёт 0, а не KeyError, плюс есть most_common() и арифметика мультимножеств.

from collections import Counter

c = Counter("abracadabra")
c.most_common(2)      # [('a', 5), ('b', 2)]
c["z"]                # 0 — и ключ при этом НЕ создаётся
Counter("aab") - Counter("ab")     # Counter({'a': 1})  — отрицательные отброшены
Counter("aab") + Counter("b")      # Counter({'a': 2, 'b': 2})

Подвох. - и + выбрасывают неположительные значения, а subtract() — нет, он честно оставит отрицательные счётчики. Это разные операции, и на собеседовании обычно спрашивают именно про -.


Когда нужен deque вместо списка?

Коротко. Когда добавляют или удаляют элементы с обоих концов. У deque это O(1) с каждой стороны, у списка insert(0, x) и pop(0) — O(n), потому что весь массив указателей сдвигается.

from collections import deque

dq = deque([1, 2, 3], maxlen=3)
dq.appendleft(0)
print(dq)
# deque([0, 1, 2], maxlen=3)  — 3 вытеснено справа

maxlen даёт готовое кольцо фиксированной длины (окно последних N событий, «последние 100 строк лога»), rotate(n) циклически сдвигает содержимое.

Подвох. Плата за быстрые концы — доступ по индексу: dq[i] в середине это O(n), а срезы dq[1:3] вообще не поддерживаются. deque — очередь, а не замена списку.

Глубже. Внутри — двусвязный список блоков по 64 указателя, а не один непрерывный массив, как у list. Отсюда и O(1) на концах, и потеря произвольного доступа. deque потокобезопасен для append/popleft — атомарность обеспечивает сама реализация на уровне C-кода. В free-threaded сборке (3.13t/3.14t, без GIL) та же атомарность сохраняется, но уже через явные per-object критические секции, добавленные в реализацию deque именно под no-GIL сборку, а не через GIL, которого там нет. Поэтому deque берут как простую очередь между потоками, когда не нужны блокировки queue.Queue.


Зачем namedtuple, если есть dataclass?

Коротко. namedtuple — это настоящий кортеж с именами полей: неизменяемый, хешируемый, распаковывается, сравнивается поэлементно и не занимает лишней памяти. dataclass — изменяемый объект с __dict__ (или __slots__) и куда более гибкий.

from collections import namedtuple

Point = namedtuple("Point", "x y")
p = Point(1, 2)
p.x                 # 1
p._replace(y=5)     # Point(x=1, y=5) — новый объект, старый не изменился
p._asdict()         # {'x': 1, 'y': 2}
isinstance(p, tuple)  # True

Подчёркивание в _replace/_asdict — не «приватность», а защита от конфликта с именами полей.

Подвох. Раз это кортеж — он равен обычному кортежу с теми же значениями (Point(1, 2) == (1, 2) истинно) и его можно случайно распаковать не в том порядке. Когда нужна строгая типизация полей, берут typing.NamedTuple (тот же кортеж, но с аннотациями) или dataclass (см. Классы).


Что ещё стоит назвать из collections?

  • OrderedDict — до Python 3.7 единственный словарь с гарантированным порядком. Сейчас порядок вставки гарантирован и у обычного dict, но OrderedDict остаётся полезен: у него есть move_to_end() и popitem(last=), а сравнение учитывает порядок:

    from collections import OrderedDict
    OrderedDict(a=1, b=2) == OrderedDict(b=2, a=1)   # False — порядок важен
    dict(a=1, b=2) == dict(b=2, a=1)                 # True
  • ChainMap — цепочка словарей как один: поиск идёт по слоям до первого попадания, записи уходят в первый слой. Типовое применение — слои конфигурации (аргументы → окружение → умолчания) без копирования словарей.


← Модуль itertools · 🏠 Домой · Сортировка →