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

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

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

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

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

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

Компактно и лаконично! Но что же делает метод setdefault? Метод принимает ключ и значение по умолчанию и возвращает ссылку на значение в словаре, связанное с указанным ключом. А если ключ в словаре отсутствует, то метод помещает по ключу то самое значение по умолчанию и возвращает ссылку на него!

В примере выше значением по умолчанию выступает пустой список [].

defaultdict“>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-от-обычного-словаря-c-setdefault“>Отличия defaultdict от обычного словаря c setdefault

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

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

# b это defaultdict(list)

Строки очень похожи, но если во втором случае новый список создаётся только тогда, когда ключ не будет найден, то в первой строчке объект пустого списка будет создаваться каждый раз. Конкретно затратами на создание пустого списка можно пренебречь. Однако, если вдруг затраты на создание значения по умолчанию окажутся велики, скажем, каждое создание потребует хождения в базу данных, то вариант с defaultdict сразу же окажется гораздо более предпочтителен!

Зачем же вообще использовать setdefault? Например для того, чтобы по разным ключам инициализировать разные значения! Т.к. значение по-умолчанию передаётся каждый раз, мы можем по разным ключам хранить даже разные типы данных. С defaultdict у нас нет контроля над тем, какие значения по каким ключам класть: функция-инициализатор вызывается каждый раз одна и та же и ключ в неё не передаётся.

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

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

Да, здесь присутствуют лишние хождения по одному и тому же ключу, но сам код читается неплохо и в данной ситуации, можно сказать, оптимален!

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Node, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →