W3docs

Модуль 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, ChainMap

Counter

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 preserved

defaultdict

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']}

Часто используемые фабричные функции

ФабрикаЗначение по умолчаниюТипичное применение
int0Подсчёт
float0.0Накопление сумм
list[]Группировка элементов
setset()Сбор уникальных значений
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 unchanged

namedtuple против 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?

Остаются две актуальные причины:

  1. move_to_end() — позволяет эффективно перемещать ключи в начало или конец.
  2. Равенство — два экземпляра 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 directly

new_child() возвращает новый ChainMap с пустым словарём, добавленным в начало, — именно так моделируются встроенные правила области видимости Python (локальная → охватывающая → глобальная → встроенная).

Когда использовать ChainMap: многоуровневая конфигурация (переопределения пользователя → настройки проекта → глобальные настройки), реализация областей видимости, объединение аргументов командной строки с переменными окружения и конфигурационными файлами.


Выбор правильного типа

Вам нужно…Используйте
Подсчитать вхождения элементовCounter
Избежать KeyError с помощью значения по умолчаниюdefaultdict
Представить запись с именованными полямиnamedtuple
Быстрое добавление/удаление с обоих концов или ограниченный буферdeque
Сравнение словарей с учётом порядка или move_to_end()OrderedDict
Объединить несколько словарей в одно представление без копированияChainMap

Подробнее о базовых типах, которые они расширяют, читайте в Python Dictionaries, Python Lists и Python Tuples. Об итераторных вспомогательных функциях в стандартной библиотеке см. Модуль itertools в Python.

Практика

Практика
Which collections type returns 0 (instead of raising KeyError) when you access a missing key and count occurrences automatically?
Which collections type returns 0 (instead of raising KeyError) when you access a missing key and count occurrences automatically?
Практика
A deque with maxlen=3 already holds [1, 2, 3]. What does it contain after append(4) is called?
A deque with maxlen=3 already holds [1, 2, 3]. What does it contain after append(4) is called?
Практика
Which statement about OrderedDict is true in Python 3.7 and later?
Which statement about OrderedDict is true in Python 3.7 and later?
Was this page helpful?