Модуль collections в Python
Изучите модуль collections в Python: Counter, defaultdict, namedtuple, deque, OrderedDict и ChainMap — с практическими примерами и рекомендациями по применению.
Встроенный модуль collections в Python предоставляет специализированные типы контейнеров, расширяющие или заменяющие стандартные list, dict и tuple. Каждый тип решает конкретную задачу, которая в противном случае потребовала бы нескольких дополнительных строк ручного кода.
В этой главе рассматриваются все шесть часто используемых типов: Counter, defaultdict, namedtuple, deque, OrderedDict и ChainMap. Для каждого из них вы увидите, какую проблему он решает, как его создать и использовать, а также на что следует обратить внимание.
Установка не требуется — collections входит в каждую установку Python 3:
from collections import Counter, defaultdict, namedtuple, deque, OrderedDict, ChainMapCounter
Counter — это подкласс dict, предназначенный для подсчёта хешируемых объектов. Вы передаёте ему итерируемый объект (или строку, или именованные аргументы), и он возвращает словарь-подобный объект, где ключи — это элементы, а значения — их количество.
Создание Counter
from collections import Counter
# From a list
word_list = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
c = Counter(word_list)
print(c)
# Output: Counter({'apple': 3, 'banana': 2, 'cherry': 1})При обращении к отсутствующим ключам возвращается 0 вместо вызова KeyError:
print(c['apple']) # 3
print(c['mango']) # 0 — no KeyErrorНаиболее частые элементы
most_common(n) возвращает n элементов с наибольшим количеством в виде списка кортежей (element, count), отсортированных от наиболее к наименее частым:
print(c.most_common(2))
# Output: [('apple', 3), ('banana', 2)]Если не передать n, возвращаются все элементы, отсортированные по частоте.
Арифметика Counter
Counter поддерживает сложение, вычитание, пересечение и объединение:
a = Counter(['a', 'a', 'b']) # Counter({'a': 2, 'b': 1})
b = Counter(['a', 'b', 'b', 'c']) # Counter({'b': 2, 'a': 1, 'c': 1})
print(a + b) # Counter({'a': 3, 'b': 3, 'c': 1})
print(a - b) # Counter({'a': 1}) — only positive counts kept
print(a & b) # Counter({'a': 1, 'b': 1}) — minimum of each count
print(a | b) # Counter({'a': 2, 'b': 2, 'c': 1}) — maximum of each countКогда использовать Counter: подсчёт голосов, частота слов, подсчёт символов, построение гистограмм.
Подводный камень Counter: вычитание сохраняет только положительные значения
a - b молча отбрасывает элементы, где результат был бы равен нулю или отрицательным. Если вам нужно сохранить отрицательные счётчики, используйте subtract():
a = Counter({'x': 2})
b = Counter({'x': 5})
a.subtract(b)
print(a) # Counter({'x': -3}) — negative count preserveddefaultdict
defaultdict — это подкласс dict, который вызывает фабричную функцию для получения значения по умолчанию при обращении к ещё несуществующему ключу. Это устраняет необходимость в защитных конструкциях вида if key not in d:.
Создание defaultdict
Передайте фабрику первым аргументом:
from collections import defaultdict
dd = defaultdict(int) # default value: int() == 0
words = ['cat', 'dog', 'cat', 'bird', 'dog', 'cat']
for word in words:
dd[word] += 1 # no KeyError on first access
print(dict(dd))
# Output: {'cat': 3, 'dog': 2, 'bird': 1}Без defaultdict потребовалось бы писать dd[word] = dd.get(word, 0) + 1 или использовать Counter.
Группировка элементов с фабрикой list
groups = defaultdict(list)
data = [('fruit', 'apple'), ('veggie', 'carrot'), ('fruit', 'banana'), ('veggie', 'broccoli')]
for category, item in data:
groups[category].append(item)
print(dict(groups))
# Output: {'fruit': ['apple', 'banana'], 'veggie': ['carrot', 'broccoli']}Часто используемые фабричные функции
| Фабрика | Значение по умолчанию | Типичное применение |
|---|---|---|
int | 0 | Подсчёт |
float | 0.0 | Накопление сумм |
list | [] | Группировка элементов |
set | set() | Сбор уникальных значений |
str | '' | Формирование строк |
dict | {} | Вложенные отображения |
Вы также можете передать лямбда-функцию без аргументов для собственного значения по умолчанию: defaultdict(lambda: 'N/A').
Подводный камень defaultdict: обращение к ключу создаёт его
В отличие от dict.get(), простое обращение dd[key] к отсутствующему ключу вставляет этот ключ со значением по умолчанию. Это может удивить при итерации или проверке принадлежности:
dd = defaultdict(int)
print('foo' in dd) # False — key does not exist yet
_ = dd['foo'] # access inserts the key
print('foo' in dd) # True — key was silently createdИспользуйте dd.get('foo') или 'foo' in dd, если хотите проверить ключ без побочных эффектов.
Когда использовать defaultdict: группировка данных, построение списков смежности для графов, любой шаблон, где сначала выполняется инициализация, а затем обновление.
namedtuple
namedtuple создаёт новый класс, экземпляры которого являются обычными кортежами, но с именованными полями. Результат неизменяем, эффективно использует память (нет __dict__ на каждый экземпляр) и самодокументируется.
Создание namedtuple
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(3, 7)
print(p) # Point(x=3, y=7)
print(p.x) # 3
print(p.y) # 7
print(p[0]) # 3 — index access still worksПервый аргумент namedtuple() — это имя типа (используется в repr). Второй аргумент — список имён полей (или строка, разделённая пробелами или запятыми: 'x y').
Практический пример
Employee = namedtuple('Employee', ['name', 'department', 'salary'])
emp = Employee('Alice', 'Engineering', 95000)
print(emp.name, emp.department, emp.salary)
# Output: Alice Engineering 95000Именованный доступ (emp.name) гораздо понятнее позиционного (row[0]) при чтении данных из CSV-файлов или строк базы данных.
Полезные методы namedtuple
# Convert to an ordered dictionary
print(p._asdict()) # {'x': 3, 'y': 7}
# Create a modified copy (namedtuples are immutable)
p2 = p._replace(x=10)
print(p2) # Point(x=10, y=7)
print(p) # Point(x=3, y=7) — original unchangednamedtuple против dataclass
В Python 3.7 появился dataclasses.dataclass как альтернатива. Выбирайте namedtuple, когда нужна неизменяемость и полная совместимость с кортежем (распаковка, индексирование, хеширование). Выбирайте dataclass, когда нужны изменяемые поля, фабрики значений по умолчанию или методы.
Когда использовать namedtuple: представление записей (строки базы данных, строки CSV, пары координат, цвета RGB), когда важна неизменяемость и экономия памяти.
deque
deque (двусторонняя очередь, произносится «дек») — это последовательность, оптимизированная для O(1) добавления и удаления элементов с обоих концов. Обычный список обеспечивает O(1) для append и O(n) для insert(0, …); deque обеспечивает O(1) с обоих концов.
Создание deque
from collections import deque
d = deque([1, 2, 3])
print(d) # deque([1, 2, 3])Добавление и удаление элементов
d.append(4) # add to right
d.appendleft(0) # add to left
print(d) # deque([0, 1, 2, 3, 4])
d.pop() # remove from right → 4
d.popleft() # remove from left → 0
print(d) # deque([1, 2, 3])Вращение deque
rotate(n) сдвигает элементы вправо на n позиций (отрицательное значение — влево):
d = deque([1, 2, 3])
d.rotate(1)
print(d) # deque([3, 1, 2])
d.rotate(-1)
print(d) # deque([1, 2, 3])Ограниченный deque (скользящее окно / буфер FIFO)
Параметр maxlen ограничивает размер deque. При добавлении новых элементов сверх ограничения элементы с противоположного конца автоматически удаляются — идеально для хранения последних N событий:
buffer = deque(maxlen=3)
for i in range(5):
buffer.append(i)
print(buffer) # deque([2, 3, 4], maxlen=3)Подводный камень deque: медленный O(n) произвольный доступ
deque не поддерживает эффективный произвольный доступ. d[500] выполняется за O(n), а не O(1), как в списке. Если вы часто обращаетесь к элементам по индексу, используйте list. Применяйте deque только тогда, когда нужно быстрое добавление и удаление с обоих концов.
Когда использовать deque: реализация очередей и стеков, алгоритмы скользящего окна, обход в ширину (BFS), хранение последних N записей лога.
OrderedDict
Начиная с Python 3.7, обычный dict сохраняет порядок вставки. Зачем тогда использовать OrderedDict?
Остаются две актуальные причины:
move_to_end()— позволяет эффективно перемещать ключи в начало или конец.- Равенство — два экземпляра
OrderedDictс одинаковыми ключами, но разным порядком вставки, считаются неравными, в отличие от обычных словарей.
Создание и изменение порядка в OrderedDict
from collections import OrderedDict
od = OrderedDict()
od['one'] = 1
od['two'] = 2
od['three'] = 3
print(list(od.keys())) # ['one', 'two', 'three']
od.move_to_end('one') # move 'one' to the end
print(list(od.keys())) # ['two', 'three', 'one']
od.move_to_end('three', last=False) # move 'three' to the front
print(list(od.keys())) # ['three', 'two', 'one']Равенство с учётом порядка
od1 = OrderedDict([('a', 1), ('b', 2)])
od2 = OrderedDict([('b', 2), ('a', 1)])
print(od1 == od2) # False — different order
d1 = {'a': 1, 'b': 2}
d2 = {'b': 2, 'a': 1}
print(d1 == d2) # True — regular dicts ignore orderКогда использовать OrderedDict: реализация LRU-кэша (перемещение недавно использованного ключа в конец), любой алгоритм, где порядок вставки должен влиять на равенство.
ChainMap
ChainMap объединяет несколько словарей в единое логическое представление. Поиск осуществляется по словарям в порядке их следования; записи и удаления всегда затрагивают только первый словарь.
Базовое использование
from collections import ChainMap
defaults = {'color': 'blue', 'size': 'medium', 'theme': 'light'}
overrides = {'color': 'red', 'size': 'large'}
combined = ChainMap(overrides, defaults)
print(combined['color']) # 'red' — found in overrides first
print(combined['theme']) # 'light' — not in overrides, falls back to defaultsЗаписи попадают только в первый словарь:
combined['font'] = 'serif'
print(overrides) # {'color': 'red', 'size': 'large', 'font': 'serif'}
print(defaults) # {'color': 'blue', 'size': 'medium', 'theme': 'light'} — unchangedИмитация областей видимости с помощью new_child()
base = ChainMap({'x': 1})
child = base.new_child({'x': 99, 'y': 2})
print(child['x']) # 99 — child scope shadows parent
print(child['y']) # 2
print(child.parents['x']) # 1 — access parent scope directlynew_child() возвращает новый ChainMap с пустым словарём, добавленным в начало, — именно так моделируются встроенные правила области видимости Python (локальная → охватывающая → глобальная → встроенная).
Когда использовать ChainMap: многоуровневая конфигурация (переопределения пользователя → настройки проекта → глобальные настройки), реализация областей видимости, объединение аргументов командной строки с переменными окружения и конфигурационными файлами.
Выбор правильного типа
| Вам нужно… | Используйте |
|---|---|
| Подсчитать вхождения элементов | Counter |
Избежать KeyError с помощью значения по умолчанию | defaultdict |
| Представить запись с именованными полями | namedtuple |
| Быстрое добавление/удаление с обоих концов или ограниченный буфер | deque |
Сравнение словарей с учётом порядка или move_to_end() | OrderedDict |
| Объединить несколько словарей в одно представление без копирования | ChainMap |
Подробнее о базовых типах, которые они расширяют, читайте в Python Dictionaries, Python Lists и Python Tuples. Об итераторных вспомогательных функциях в стандартной библиотеке см. Модуль itertools в Python.