defaultdict : эволюция словаря без условий

В Python существует множество внешних библиотек, но даже при правильном понимании стандартной библиотеки можно писать достаточно мощный код для практики. В этой статье мы глубоко рассмотрим collections.defaultdict.

Вы поймёте, когда, почему и как использовать defaultdict, выходя за рамки простого введения в концепцию.

Тем, кто интересуется первым классом в серии collections, классов Counter, я рекомендую прочитать предыдущую статью. Стандартная библиотека Python ① - collections.Counter


1. Основная концепция: Что такое defaultdict?

Tux with defaultdicts

defaultdict - это специальный подкласс словаря (dict), входящий в модуль collections стандартной библиотеки Python. Когда вы обращаетесь к ключу, которого нет в обычном словаре, возникает KeyError, но defaultdict позволяет указать функцию для автоматического создания значения по умолчанию (фабричную функцию), что делает код намного чище и помогает избежать ошибок.


2. Основное использование

from collections import defaultdict

d = defaultdict(int)
d['apple'] += 1
print(d)  # defaultdict(<class 'int'>, {'apple': 1})

Здесь int() возвращает значение по умолчанию 0. При обращении к несуществующему ключу 'apple' автоматически создаётся 0 без возникновения KeyError, и затем выполняется +1.


3. Примеры различных значений по умолчанию

from collections import defaultdict

# Значение по умолчанию: 0 (int)
counter = defaultdict(int)
counter['a'] += 1
print(counter)  # defaultdict(<class 'int'>, {'a': 1})

# Значение по умолчанию: пустой список
group = defaultdict(list)
group['fruit'].append('apple')
group['fruit'].append('banana')
print(group)  # defaultdict(<class 'list'>, {'fruit': ['apple', 'banana']})

# Значение по умолчанию: пустое множество
unique_tags = defaultdict(set)
unique_tags['tags'].add('python')
unique_tags['tags'].add('coding')
print(unique_tags)  # defaultdict(<class 'set'>, {'tags': {'python', 'coding'}})

# Значение по умолчанию: пользовательское начальное значение
fixed = defaultdict(lambda: 100)
print(fixed['unknown'])  # 100

4. Практические примеры

1. Подсчёт частоты слов

words = ['apple', 'banana', 'apple', 'orange', 'banana']
counter = defaultdict(int)

for word in words:
    counter[word] += 1

print(counter)
# defaultdict(<class 'int'>, {'apple': 2, 'banana': 2, 'orange': 1})

👉 Counter vs defaultdict
Поскольку collections.Counter() более специализирован для подсчёта слов, в случаях, когда необходима статистика или анализ рангов, предпочтительнее использовать Counter. Однако для простого суммирования, как при накоплении счётов, defaultdict(int) также вполне достаточно и может использоваться с простотой.

2. Группировка логов

logs = [
    ('2024-01-01', 'INFO'),
    ('2024-01-01', 'ERROR'),
    ('2024-01-02', 'DEBUG'),
]

grouped = defaultdict(list)
for date, level in logs:
    grouped[date].append(level)

print(grouped)
# defaultdict(<class 'list'>, {'2024-01-01': ['INFO', 'ERROR'], '2024-01-02': ['DEBUG']})

3. Упорядочение уникальных тегов

entries = [
    ('post1', 'python'),
    ('post1', 'coding'),
    ('post1', 'python'),
]

tags = defaultdict(set)
for post, tag in entries:
    tags[post].add(tag)

print(tags)
# defaultdict(<class 'set'>, {'post1': {'python', 'coding'}})

5. Важные моменты

  • defaultdict хранит генератор значений по умолчанию внутри, поэтому при использовании repr() может иметь другой вид, чем обычный dict.
  • Это может вызвать проблемы при сериализации JSON. Лучше всего преобразовать его в dict(d) перед обработкой.
  • Значения по умолчанию создаются только при доступе через []. При доступе через get() они не создаются.
from collections import defaultdict

d = defaultdict(list)
print(d.get('missing'))  # None
print(d['missing'])      # []

6. Когда это полезно? – 3 критических преимущества defaultdict

defaultdict повышает читаемость, поддерживаемость и безопасность в ситуациях, когда часто используются паттерны dict + условие. Особенно в следующих трёх ситуациях осознаете: «Да, здесь определённо лучше использовать defaultdict!».

6-1. Код агрегирования без условий

from collections import defaultdict

# Обычный dict
counts = {}
for item in items:
    if item not in counts:
        counts[item] = 0
    counts[item] += 1

# defaultdict
counts = defaultdict(int)
for item in items:
    counts[item] += 1

✔ Устранение условного оператора делает код более лаконичным и снижает вероятность ошибок.
✔ Особенно подходит для обработки больших данных, таких как анализ логов и подсчёт слов.

6-2. Замена setdefault при накоплении списков/множеств

from collections import defaultdict

# Обычный dict
posts = {}
for tag, post in data:
    if tag not in posts:
        posts[tag] = []
    posts[tag].append(post)

# defaultdict
posts = defaultdict(list)
for tag, post in data:
    posts[tag].append(post)

✔ Намного более интуитивно и аккуратно, даже в цикле.
✔ Структура оптимизирована для группировки данных.

6-3. Автоматизация инициализации вложенных словарей

# Обычный словарь
matrix = {}
if 'x' not in matrix:
    matrix['x'] = {}
matrix['x']['y'] = 10

# Вложенный defaultdict
matrix = defaultdict(lambda: defaultdict(int))  # При отсутствии ключа автоматически создаётся defaultdict(int)
matrix['x']['y'] += 10

✔ Упрощает создание вложенных структур в многомерных словарях.
✔ Идеально подходит для добычи данных, парсинга и хранения древовидных структур.

lambda: defaultdict(int) имеет структуру, возврат которой defaultdict(int) создаётся автоматически при отсутствии ключа.


7. Резюме

collections.defaultdict может выглядеть для новичков как простая расширенная версия dict, но по мере его использования вы поймёте, что это структурный инструмент, делающий код более ясным и безопасным.

  • Вы можете использовать словарь, не беспокоясь о KeyError.
  • Можно группировать и накапливать данные без условий.
  • Можно интуитивно строить вложенные словари.
# Пример безопасной и лаконичной обработки с помощью defaultdict
salaries = defaultdict(int)
for dept, amount in records:
    salaries[dept] += amount

Если вы можете избежать ошибок всего одной строкой кода + улучшить читаемость + поддерживаемость, то
defaultdict - это не просто полезная функция, а основной инструмент мышления на Python.

Следующей темой будет pathlib.
Это современный способ, который сделает код для работы с файлами/каталогами объектно-ориентированным, и будет полезен как новичкам, так и более опытным пользователям. Разработчики, знакомые с os.path, почувствуют: «Ух ты! Это гораздо проще, чем os.path!». Ожидайте следующей части!