Представьте ситуацию: вам нужно хранить в словаре в качестве значений списки или любые другие изменяемые данные. У вас есть ключ и элемент для добавления в список-значение, но сам ключ в словаре может быть не представлен. В таком случае придется писать подобный код:
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
Обратите внимание, что в этом коде присутствуют лишние хождения по одному и тому же ключу. При этом сам код читается неплохо — в данной ситуации его можно назвать оптимальным.
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.