Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Инициализация новых значений и defaultdicts Python: словари и множества

Представьте ситуацию: вам нужно хранить в словаре в качестве значений списки или любые другие изменяемые данные. У вас есть ключ и элемент для добавления в список-значение, но сам ключ в словаре может быть не представлен. В таком случае придется писать подобный код:

if key not in dictionary:
    dictionary[key] = []  # инициализируем список
dictionary[key].append(value)  # изменяем список

Подобная ситуация встречается не так уж и редко. Это понимали и авторы стандартной библиотеки Python и дали словарю метод setdefault. Именно этот метод мы рассмотрим подробнее в этом уроке.

Инициализация новых значений

Попробуем переписать код выше с помощью метода setdefault:

dictionary.setdefault(key, []).append(value)

Метод setdefault принимает ключ и значение по умолчанию, а затем возвращает ссылку на значение в словаре, связанное с указанным ключом. Если ключ в словаре отсутствует, то метод помещает по ключу то самое значение по умолчанию и возвращает ссылку на него. В примере выше значением по умолчанию выступает пустой список [].

Тип defaultdict

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

from collections import defaultdict
d = defaultdict(int)
d['a'] += 5
d['b'] = d['c'] + 10
d  # defaultdict(<class 'int'>, {'a': 5, 'c': 0, 'b': 10})

При создании словаря мы указали в качестве аргумента функцию int. Если эту функцию вызвать без аргументов, то она вернет 0. Именно этот вызов внутри словаря d и происходит всякий раз, когда нужно получить значение для несуществующего ключа.

В примере выше d['a'] += 5 дает 5, потому что этот код работает так:

  • Сначала для ключа 'a' создается начальное значение — делается вызов int() и получается 0
  • Уже потом к нему прибавляется 5

В строчке d['b'] = d['c'] + 10 создаются значения для ключей 'b' и 'c'. Затем уже по ключу 'b' записывается сумма 0 + 10.

Вот еще один пример — на этот раз с самодельной функцией-инициализатором:

def new_value():
    return 'foo'
x = defaultdict(new_value)
x[1]  # 'foo'
x['bar']  # 'foo'
x  # defaultdict(<function new_value at 0x7f2232cf5a60>, {1: 'foo', 'bar': 'foo'})

Попробуем отбросить немного непонятное упоминание функции-инициализатора. Так станет видно, что теперь строки 'foo' записаны по всем ключам, по которым мы обращались к содержимому словаря.

Отличия defaultdict от обычного словаря с setdefault

Пока не совсем понятно, зачем иметь оба способа, если они настолько похожи. Но давайте сравним эти две строки:

a.setdefault(key, []).append
# vs
b[key].append

# b — это defaultdict(list)

Строки очень похожи, но есть одно различие:

  • В первом случае объект пустого списка будет создаваться каждый раз
  • Во втором случае новый список создается только тогда, когда ключ не будет найден

Значения аргументов всегда вычисляются до того, как будет вызвана функция. Поэтому здесь в случае с setdefault(key, []) затратами на создание пустого списка можно пренебречь. Если вдруг затраты на создание значения по умолчанию окажутся велики, вариант с defaultdict окажется гораздо предпочтительнее.

Зачем вообще использовать setdefault? Например, он помогает инициализировать разные значения по разным ключам. Значение по умолчанию передается каждый раз, поэтому мы можем хранить по разным ключам даже разные типы данных.

С defaultdict у нас нет контроля над тем, какие значения по каким ключам класть. Функция-инициализатор вызывается каждый раз одна и та же — ключ в нее не передается.

Наконец, всегда остаются редкие случаи, когда и defaultdict не подходит. Например, если нужно инициализировать значения по-разному, но не подходит и setdefault. Новые значения неизменяемы, их не получится изменить по возвращаемой ссылке. Рассмотрим пример такого случая вместе с решением задачи ненахождения ключа:

x['count'] = x.get('count', 0) + 1
x['path'] = x.get('path', '') + '/' + dir

Обратите внимание, что в этом коде присутствуют лишние хождения по одному и тому же ключу. При этом сам код читается неплохо — в данной ситуации его можно назвать оптимальным.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
с нуля
Разработка веб-приложений на Django
2 февраля 10 месяцев
Иконка программы Аналитик данных
Профессия
В разработке с нуля
Сбор, анализ и интерпретация данных
16 марта 8 месяцев

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»