Python · Синтаксис · Промежуточный

collections: Counter, defaultdict, namedtuple

10 задач

Специализированные типы контейнеров: Counter для подсчёта, defaultdict для сгруппированных данных, namedtuple для структурированных записей.

Counter и defaultdict

#
Модуль `collections` предоставляет специализированные контейнерные типы, решающие распространённые задачи чище, чем обычные dict и list. **Counter — подсчёт вхождений** ```python from collections import Counter words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'] c = Counter(words) # Counter({'apple': 3, 'banana': 2, 'cherry': 1}) c['apple'] # 3 c['mango'] # 0 — отсутствующие ключи возвращают 0, не KeyError c.most_common(2) # [('apple', 3), ('banana', 2)] c.total() # 6 (Python 3.10+) # Подсчёт символов в строке Counter('mississippi') # Counter({'s': 4, 'i': 4, 'p': 2, 'm': 1}) ``` **Арифметика Counter** ```python a = Counter({'cat': 3, 'dog': 2}) b = Counter({'dog': 1, 'bird': 2}) a + b # Counter({'cat': 3, 'dog': 3, 'bird': 2}) — объединение (сумма) a - b # Counter({'cat': 3, 'dog': 1}) — разность (убирает отрицательные) a & b # Counter({'dog': 1}) — пересечение (минимум) a | b # Counter({'cat': 3, 'bird': 2, 'dog': 2}) — объединение (максимум) ``` **defaultdict — автоматические значения по умолчанию** `defaultdict` вызывает фабричную функцию для создания отсутствующих значений вместо `KeyError`: ```python from collections import defaultdict # Группировка слов по первой букве by_letter = defaultdict(list) # фабрика: list -> по умолчанию [] for word in ['apple', 'avocado', 'banana', 'blueberry']: by_letter[word[0]].append(word) # defaultdict({'a': ['apple', 'avocado'], 'b': ['banana', 'blueberry']}) # Подсчёт без Counter freq = defaultdict(int) # фабрика: int -> по умолчанию 0 for ch in 'hello world': freq[ch] += 1 # Вложенные словари graph = defaultdict(set) # список смежности для графа graph['A'].add('B') graph['A'].add('C') ``` **defaultdict vs dict.get vs dict.setdefault** ```python d = {} # Обычный dict — три подхода к отсутствующим ключам: d.get('key', 0) + 1 # чтение с дефолтом, но не сохраняет его d.setdefault('key', []).append(1) # сохраняет дефолт при первом доступе # defaultdict — чище всего, когда все отсутствующие ключи одного типа dd = defaultdict(list) dd['key'].append(1) # не нужна специальная обработка ```

deque (с maxlen) и namedtuple

#
**deque — двусторонняя очередь** `deque` (произносится 'дек') поддерживает O(1) добавление и удаление с обоих концов. В обычном списке `insert(0, x)` и `pop(0)` — O(n), т.к. все элементы сдвигаются. ```python from collections import deque d = deque([1, 2, 3]) d.append(4) # [1, 2, 3, 4] — добавить справа d.appendleft(0) # [0, 1, 2, 3, 4] — добавить слева, O(1) d.pop() # 4, d = [0, 1, 2, 3] d.popleft() # 0, d = [1, 2, 3] d.rotate(1) # [3, 1, 2] — вращение вправо на 1 d.rotate(-1) # [1, 2, 3] — вращение влево на 1 ``` **maxlen — скользящее окно / буфер фиксированного размера** Когда у `deque` есть `maxlen`, новые элементы автоматически вытесняют старые с другого конца: ```python # Хранить последние 5 введённых команд history = deque(maxlen=5) for cmd in ['ls', 'cd /tmp', 'cat file', 'pwd', 'ls -la', 'whoami']: history.append(cmd) list(history) # ['cd /tmp', 'cat file', 'pwd', 'ls -la', 'whoami'] # 'ls' был вытеснен, когда пришёл 'whoami', потому что maxlen=5 # Скользящее среднее window = deque(maxlen=3) for reading in [10, 20, 30, 40, 50]: window.append(reading) print(sum(window) / len(window)) # 10.0, 15.0, 20.0, 30.0, 40.0 ``` **namedtuple — лёгкий тип записи** ```python from collections import namedtuple Point = namedtuple('Point', ['x', 'y']) p = Point(3, 4) p.x # 3 — доступ по атрибуту p[0] # 3 — доступ по индексу (это всё ещё кортеж) p.x, p.y # 3, 4 x, y = p # распаковка тоже работает ``` **Полезные методы namedtuple** ```python Point._fields # ('x', 'y') Point._make([3, 4]) # Point(x=3, y=4) из любого итерируемого p._replace(x=10) # Point(x=10, y=4) — возвращает новый экземпляр p._asdict() # {'x': 3, 'y': 4} ``` **namedtuple vs dict vs dataclass** ``` namedtuple — неизменяемый, совместимый с кортежем, очень экономный по памяти dict — изменяемый, гибкие ключи, чуть больше памяти dataclass — изменяемый по умолчанию, поддерживает методы, type hints, __post_init__ ``` Если поля фиксированы и мутация не нужна, `namedtuple` — самый лёгкий выбор.

OrderedDict, ChainMap и выбор правильной коллекции

#
**OrderedDict — словарь, запоминающий порядок вставки** В Python 3.7+ обычный `dict` тоже сохраняет порядок вставки, поэтому `OrderedDict` редко нужен. Он полезен для метода `.move_to_end()` и когда хочешь явно обозначить, что порядок важен: ```python from collections import OrderedDict od = OrderedDict() od['a'] = 1 od['b'] = 2 od['c'] = 3 od.move_to_end('a') # переместить 'a' в конец list(od) # ['b', 'c', 'a'] od.move_to_end('c', last=False) # переместить 'c' в начало list(od) # ['c', 'b', 'a'] # LRU-кеш (вытеснение давно неиспользуемого) class LRUCache: def __init__(self, capacity): self.cache = OrderedDict() self.cap = capacity def get(self, key): if key not in self.cache: return -1 self.cache.move_to_end(key) # пометить как недавно использованный return self.cache[key] def put(self, key, value): self.cache[key] = value self.cache.move_to_end(key) if len(self.cache) > self.cap: self.cache.popitem(last=False) # вытеснить самый старый ``` **Выбор правильной коллекции** ``` Потребность Используй ──────────────────────────────────── ───────────────────── Подсчёт вхождений Counter Группировка, авто-инит отсутствующих ключей defaultdict(list) Накопление без KeyError для новых ключей defaultdict(int/float) Быстрая очередь (оба конца) deque Скользящее окно фиксированного размера deque(maxlen=N) Лёгкая неизменяемая запись namedtuple Изменяемая запись с методами dataclass Словарь с явным управлением порядком OrderedDict Всё остальное dict / list ``` **ChainMap — объединение нескольких словарей без копирования** ```python from collections import ChainMap defaults = {'color': 'red', 'size': 'M'} overrides = {'color': 'blue'} merged = ChainMap(overrides, defaults) merged['color'] # 'blue' — найдено в overrides первым merged['size'] # 'M' — проваливается до defaults # Записи идут в первый словарь merged['weight'] = 'heavy' overrides # {'color': 'blue', 'weight': 'heavy'} ``` Полезен для слоёной конфигурации (настройки пользователя перекрывают умолчания) и для поиска переменных по областям видимости.
01

#

Используйте `Counter`, чтобы посчитать, сколько раз каждое слово встречается в списке. Верните объект Counter.

from collections import Counter

def word_count(words):
    # ваш код здесь
    pass

words = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple']
c = word_count(words)
print(c['apple'])   # 3
print(c['banana'])  # 2
print(c['grape'])   # 0 (Counter возвращает 0 для отсутствующих ключей)
Решение
from collections import Counter

def word_count(words):
    return Counter(words)
02

#

Верните 3 наиболее частых слова в списке слов.

from collections import Counter

def top_three(words):
    # ваш код здесь
    pass

words = ['the', 'cat', 'sat', 'on', 'the', 'mat', 'the', 'cat', 'is', 'fat']
print(top_three(words))  # [('the', 3), ('cat', 2), ('sat', 1)] или похожее
Решение
from collections import Counter

def top_three(words):
    return Counter(words).most_common(3)
03

#

Сгруппируйте список слов по первой букве с помощью `defaultdict`. Верните словарь, где каждый ключ — буква, а значение — список слов.

from collections import defaultdict

def group_by_letter(words):
    # ваш код здесь
    pass

words = ['apple', 'banana', 'avocado', 'blueberry', 'cherry']
result = group_by_letter(words)
print(result['a'])  # ['apple', 'avocado']
print(result['b'])  # ['banana', 'blueberry']
Решение
from collections import defaultdict

def group_by_letter(words):
    groups = defaultdict(list)
    for word in words:
        groups[word[0]].append(word)
    return dict(groups)
04

#

Используйте `defaultdict(int)`, чтобы подсчитать частоты букв в строке. Верните defaultdict.

from collections import defaultdict

def letter_freq(s):
    # ваш код здесь
    pass

freq = letter_freq('hello')
print(freq['l'])  # 2
print(freq['h'])  # 1
print(freq['z'])  # 0
Решение
from collections import defaultdict

def letter_freq(s):
    freq = defaultdict(int)
    for c in s:
        freq[c] += 1
    return freq
05

#

Используйте `OrderedDict` для создания кэша, хранящего последние 3 уникальных добавленных элемента (LRU-подобный). Реализуйте методы `add(key, value)` и `get_all()`.

from collections import OrderedDict

class SmallCache:
    def __init__(self):
        self.cache = OrderedDict()
        self.max_size = 3

    def add(self, key, value):
        # ваш код здесь
        pass

    def get_all(self):
        return list(self.cache.items())

c = SmallCache()
c.add('a', 1)
c.add('b', 2)
c.add('c', 3)
c.add('d', 4)  # 'a' должен быть вытеснен
print(c.get_all())  # [('b', 2), ('c', 3), ('d', 4)]
Решение
from collections import OrderedDict

class SmallCache:
    def __init__(self):
        self.cache = OrderedDict()
        self.max_size = 3

    def add(self, key, value):
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.max_size:
            self.cache.popitem(last=False)  # удалить самый старый

    def get_all(self):
        return list(self.cache.items())
06

#

Используйте `deque` для реализации скользящего максимума окна: для списка чисел и размера окна k верните список максимального значения в каждом окне.

from collections import deque

def sliding_max(nums, k):
    # ваш код здесь
    pass

print(sliding_max([1, 3, -1, -3, 5, 3, 6, 7], 3))
# [3, 3, 5, 5, 6, 7]
Решение
from collections import deque

def sliding_max(nums, k):
    result = []
    window = deque()  # хранит индексы
    for i, n in enumerate(nums):
        while window and nums[window[-1]] <= n:
            window.pop()
        window.append(i)
        if window[0] <= i - k:
            window.popleft()
        if i >= k - 1:
            result.append(nums[window[0]])
    return result
07

#

Используйте `namedtuple` для создания типа `Point` с полями `x` и `y`. Верните функцию, вычисляющую расстояние между двумя точками.

from collections import namedtuple

Point = namedtuple('Point', ['x', 'y'])

def distance(p1, p2):
    # ваш код здесь
    pass

a = Point(0, 0)
b = Point(3, 4)
print(distance(a, b))  # 5.0
Решение
from collections import namedtuple
import math

Point = namedtuple('Point', ['x', 'y'])

def distance(p1, p2):
    return math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2)
08

#

Для двух Counter (частоты слов из двух текстов) верните новый Counter с объединёнными частотами слов.

from collections import Counter

def combine_frequencies(c1, c2):
    # ваш код здесь
    pass

c1 = Counter({'apple': 3, 'banana': 1})
c2 = Counter({'apple': 2, 'cherry': 4})
result = combine_frequencies(c1, c2)
print(result['apple'])   # 5
print(result['banana'])  # 1
print(result['cherry'])  # 4
Решение
from collections import Counter

def combine_frequencies(c1, c2):
    return c1 + c2
09

#

Используйте `deque` с `maxlen` для хранения только последних N элементов, добавленных в поток. Реализуйте класс `StreamBuffer` с методами `push(item)` и `get_recent()`.

from collections import deque

class StreamBuffer:
    def __init__(self, maxlen):
        # ваш код здесь
        pass

    def push(self, item):
        # ваш код здесь
        pass

    def get_recent(self):
        return list(self.buffer)

buf = StreamBuffer(3)
for x in [1, 2, 3, 4, 5]:
    buf.push(x)
print(buf.get_recent())  # [3, 4, 5]
Решение
from collections import deque

class StreamBuffer:
    def __init__(self, maxlen):
        self.buffer = deque(maxlen=maxlen)

    def push(self, item):
        self.buffer.append(item)

    def get_recent(self):
        return list(self.buffer)
10

#

Для списка транзакций (каждая — словарь с 'category' и 'amount') используйте `defaultdict` для вычисления общей суммы по категории.

from collections import defaultdict

def totals_by_category(transactions):
    # ваш код здесь
    pass

txns = [
    {'category': 'food',    'amount': 12.5},
    {'category': 'travel',  'amount': 200.0},
    {'category': 'food',    'amount': 8.0},
    {'category': 'travel',  'amount': 50.0},
    {'category': 'books',   'amount': 25.0},
]
result = totals_by_category(txns)
print(result['food'])    # 20.5
print(result['travel'])  # 250.0
Решение
from collections import defaultdict

def totals_by_category(transactions):
    totals = defaultdict(float)
    for t in transactions:
        totals[t['category']] += t['amount']
    return dict(totals)