collections: Counter, deque, defaultdict, ChainMap
Базовые коллекции (list, tuple, dict, set) покрывают большинство задач. Но для специфичных сценариев в стандартной библиотеке есть модуль collections с готовыми решениями. В этом уроке - Counter для подсчёта частот, deque для очередей, defaultdict для группировок, ChainMap для слоистых конфигов и OrderedDict (legacy).
Counter - подсчёт частот
Counter это специальный dict для подсчёта элементов:
from collections import Counter
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counts = Counter(words)
print(counts)
# Counter({'apple': 3, 'banana': 2, 'cherry': 1})
print(counts["apple"]) # 3
print(counts["missing"]) # 0 - не KeyError, а 0
Главные методы:
counts.most_common(2) # [('apple', 3), ('banana', 2)]
counts.most_common() # все в порядке убывания
counts.elements() # iterator с повторами
# Арифметика
c1 = Counter({"a": 3, "b": 2})
c2 = Counter({"a": 1, "c": 4})
c1 + c2 # Counter({'a': 4, 'b': 2, 'c': 4})
c1 - c2 # Counter({'b': 2, 'a': 2}) - только положительные
c1 & c2 # Counter({'a': 1}) - min значений
c1 | c2 # Counter({'a': 3, 'c': 4, 'b': 2}) - max
# Обновление
counts.update(["apple", "kiwi"]) # +1 к apple, новый kiwi
counts.subtract(["apple"]) # -1 от apple
Counter удобно для:
- Подсчёта частот элементов
- Топ-N элементов
- Гистограмм
- Multiset операций
deque - двусторонняя очередь
deque (double-ended queue) - оптимизирован для добавлений/удалений с обоих концов:
from collections import deque
dq = deque([1, 2, 3])
# Добавление
dq.append(4) # справа: [1, 2, 3, 4]
dq.appendleft(0) # слева: [0, 1, 2, 3, 4]
# Удаление
dq.pop() # справа - O(1)
dq.popleft() # слева - O(1)
# Итерация
for x in dq:
print(x)
# Длина
len(dq)
Сравнение со списком:
| Операция | list | deque |
|---|---|---|
| append (справа) | O(1) | O(1) |
| pop (справа) | O(1) | O(1) |
| insert(0, x) | O(n) | O(1) через appendleft |
| pop(0) | O(n) | O(1) через popleft |
| access [i] (середина) | O(1) | O(n) |
| len | O(1) | O(1) |
deque выигрывает для FIFO (очереди), list - для random access по индексу. Про сами структуры данных - в уроке «Стек и очередь».
С ограничением максимальной длины:
last_n = deque(maxlen=5)
for x in range(10):
last_n.append(x)
print(last_n) # deque([5, 6, 7, 8, 9], maxlen=5)
Старые элементы автоматически вытесняются. Полезно для скользящих окон, лога последних N событий.
defaultdict - dict с автосозданием значений
Уже видели в уроке про словари. Подробнее:
from collections import defaultdict
# Группировка
items = [("alice", "admin"), ("bob", "user"), ("alice", "moderator")]
groups = defaultdict(list)
for name, role in items:
groups[name].append(role)
print(dict(groups))
# {'alice': ['admin', 'moderator'], 'bob': ['user']}
# Подсчёт через int
text = "hello world"
counts = defaultdict(int)
for char in text:
counts[char] += 1
# Counter был бы короче для этого, но defaultdict гибче
# Вложенный
nested = defaultdict(lambda: defaultdict(int))
nested["users"]["alice"] += 1
nested["users"]["bob"] += 2
Фабрика может быть любой callable:
def get_default():
return {"count": 0, "items": []}
data = defaultdict(get_default)
data["section_a"]["count"] += 1
data["section_a"]["items"].append("first")
OrderedDict - legacy для упорядоченности
До Python 3.7 dict был неупорядоченным, и для упорядоченности использовался OrderedDict. Сейчас обычный dict тоже упорядоченный, но OrderedDict остаётся полезен для:
from collections import OrderedDict
# move_to_end - переместить ключ в начало или конец
od = OrderedDict([("a", 1), ("b", 2), ("c", 3)])
od.move_to_end("a") # {'b': 2, 'c': 3, 'a': 1}
od.move_to_end("a", last=False) # {'a': 1, 'b': 2, 'c': 3}
# popitem с last=False - FIFO вместо LIFO
od.popitem(last=False) # ('a', 1)
# Сравнение с учётом порядка
OrderedDict([("a", 1), ("b", 2)]) == OrderedDict([("b", 2), ("a", 1)]) # False
dict([("a", 1), ("b", 2)]) == dict([("b", 2), ("a", 1)]) # True - dict игнорирует порядок при ==
Если эти специфические методы не нужны - используй обычный dict.
ChainMap - объединение нескольких dict в один view
ChainMap объединяет несколько dict в логический view без копирования. Поиск идёт по порядку - первый найденный выигрывает:
from collections import ChainMap
defaults = {"color": "red", "size": "M", "stock": 100}
user_config = {"color": "blue"}
runtime = {"stock": 50}
config = ChainMap(runtime, user_config, defaults)
print(config["color"]) # blue (из user_config)
print(config["size"]) # M (из defaults)
print(config["stock"]) # 50 (из runtime - первый источник)
Изменения идут в первый dict:
config["color"] = "green"
print(runtime) # {'stock': 50, 'color': 'green'}
print(user_config) # {'color': 'blue'} - не тронут
Полезно для:
- Слоистых конфигов (defaults < user_config < runtime overrides)
- Поиска переменной в nested scopes
- Композиции настроек без слияния в один dict
namedtuple (краткое напоминание)
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
print(p.x, p.y)
Подробно разбирали в уроке про кортежи и NamedTuple.
UserDict, UserList, UserString
Базовые классы для создания собственных коллекций через наследование:
from collections import UserDict
class LimitedDict(UserDict):
def __init__(self, max_size=10):
super().__init__()
self.max_size = max_size
def __setitem__(self, key, value):
if len(self.data) >= self.max_size and key not in self.data:
raise ValueError(f"Превышен лимит {self.max_size}")
super().__setitem__(key, value)
d = LimitedDict(max_size=2)
d["a"] = 1
d["b"] = 2
d["c"] = 3 # ValueError
Прямое наследование от dict тоже работает, но UserDict даёт более стабильное поведение (некоторые методы dict обходят __setitem__ в C-имплементации).
Чаще используется @dataclass или явная композиция, чем наследование от dict/list/str.
Когда что использовать
| Задача | Структура |
|---|---|
| Подсчёт частот | Counter |
| Топ-N элементов | Counter.most_common(n) |
| Очередь (FIFO) | deque |
| Двусторонняя очередь | deque |
| Скользящее окно | deque(maxlen=N) |
| Группировка | defaultdict(list) |
| Подсчёт + кастомная логика | defaultdict(int) |
| Слоистый конфиг | ChainMap |
| Сравнение dict с учётом порядка | OrderedDict |
| FIFO с move_to_end | OrderedDict (или dict) |
| Кастомные коллекции через наследование | UserDict/UserList/UserString |
Подводные камни
1. defaultdict создаёт ключ при доступе
d = defaultdict(list)
print(d["missing"]) # [] - но КЛЮЧ СОЗДАЁТСЯ!
print(d) # defaultdict(list, {'missing': []})
Если хочешь проверить наличие без создания - используй key in d или dict(d).get(key).
2. deque не поддерживает быстрый random access
dq = deque(range(1000))
dq[500] # O(n) - проходит до середины
Если нужен random access - используй list. Если оба - подумай о специальных структурах (например, sortedcontainers.SortedList).
3. Counter с отрицательными значениями
c = Counter(a=5)
c.subtract({"a": 10})
print(c) # Counter({'a': -5}) - отрицательные допустимы
c1 - c2 # отбрасывает отрицательные - может удивить
+ и - не симметричны с обычным dict арифметикой из-за этого.
Сравнение с Go и PHP
В Go нет встроенных аналогов Counter или deque. Используются стандартные map для подсчёта, container/list для очередей.
В PHP SplStack, SplQueue, SplObjectStorage есть в SPL, но используются нечасто - чаще через массивы с особыми соглашениями.
Python collections даёт оптимизированные структуры для типичных задач в одном модуле. Это часть «батарейки в комплекте» философии.
Мини-задание
- Топ-3 слов в тексте через Counter:
from collections import Counter
text = """python is a great language python is widely used
python developers love python collections module"""
words = text.split()
counts = Counter(words)
print(counts.most_common(3))
# [('python', 4), ('is', 2), ('great', 1)] (или похожее)
- Очередь через deque:
from collections import deque
queue = deque()
queue.append("task1")
queue.append("task2")
queue.append("task3")
while queue:
task = queue.popleft() # FIFO - O(1)
print(f"Обрабатываю: {task}")
- ChainMap для конфигов:
from collections import ChainMap
import os
defaults = {"DATABASE_URL": "sqlite:///app.db", "DEBUG": False}
env_config = {k: v for k, v in os.environ.items() if k in defaults}
config = ChainMap(env_config, defaults)
print(config["DATABASE_URL"]) # env переменная или дефолт
print(config["DEBUG"])
Что дальше
Модуль 5 завершён. Освоили все основные коллекции Python: list, tuple, dict, set и специализированные из collections. В следующем модуле перейдём к ООП: классы, наследование, dunder-методы и Protocol.