Словарь defaultdict модуля collections

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Смотреть материал на YouTube | RuTube

На этом занятии мы с вами рассмотрим словарь defaultdict из модуля collections. Как можно догадаться, defaultdict – это некоторая модификация уже существующего словаря dict языка Python. Сам класс defaultdict формируется на основе базового класса dict и поддерживает все стандартные методы и операции обычных словарей. Но зачем потребовалось определять еще один словарь? Дело в том, что если создать любой стандартный словарь, например, с одним ключом:

d = {'x': 0}

то мы можем извлечь из него значение 0 по ключу ‘x’:

d['x'] # 0

Но, указав другой не существующий ключ, получим ошибку:

d['y'] # KeyError

Это очевидное поведение словарей, но не всегда желательное. В ряде задач, при обращении к несуществующим ключам, было бы лучше назначать некоторое значение по умолчанию. Как раз этот момент автоматизирует словарь defaultdict. Он позволяет автоматически создавать значение ключа при первом обращении к нему, если этот ключ ранее отсутствовал в словаре. Давайте посмотрим, как это происходит.

Первым делом нужно создать объект словаря defaultdict. В самом простом варианте это можно сделать следующим образом:

df = defaultdict(int)

Что здесь означает int? В действительности первым аргументом следует передавать функцию (или любой вызываемый (callback) объект), которая формирует начальное значение несуществующего ключа, при первом обращении к нему. Функция int, как раз может быть вызвана без каких-либо аргументов:

int() # 0

В этом случае возвращается 0 и этот 0 устанавливается начальным значением создаваемых ключей в словаре df.

Сейчас словарь df пустой:

df # defaultdict(<class 'int'>, {})

Но, если обратиться к какому-либо ключу, например:

df['x'] # 0

то он будет автоматически создан и инициализирован значением 0. Причем инициализация выполняется только в момент создания ключа. Если ключ ‘x’ изменить:

df['x'] = 5

а потом снова обратиться к нему:

df['x'] # 5

то будет возвращено значение 5, а не ноль. Или же, если сразу создать ключ и записать в него новое значение:

df['y'] = 7 # defaultdict(<class 'int'>, {'x': 5, 'y': 7})

то это присвоенное значение в нем и будет храниться. Разумеется, значение у ключа словаря может быть любым, не только числовым, например, строковым:

df['y'] = 'abc' # defaultdict(<class 'int'>, {'x': 5, 'y': 'abc'})

То есть, defaultdict имеет абсолютно тот же базовый функционал, что и обычные словари в Python. Но с defaultdict можно сделать больше, прибавить какое-либо числовое значение к начальному значению, например, так:

df['count'] += 3 # defaultdict(<class 'int'>, {'x': 5, 'y': 7, 'count': 3})

В этом случае сначала будет создан ключ 'count' со значением 0, а затем к нему прибавляется число 3.

Этот последний пример показывает красоту применения словарей defaultdict. С помощью такой логики, например, легко реализовывать алгоритмы подсчета повторяющихся элементов в коллекциях. В программе ниже в словаре df содержатся ключи в виде имен и значений в виде их повторений в списке lst:

from collections import defaultdict
 
 
lst = ['Alex', 'Sergey', 'Fedor', 'Masha', 'Sergey', 'Fedor', 'Sergey']
df = defaultdict(int)
 
for x in lst:
    df[x] += 1
 
print(df)

После выполнения программы в консоли увидим сформированный словарь:

defaultdict(<class 'int'>, {'Alex': 1, 'Sergey': 3, 'Fedor': 2, 'Masha': 1})

Конечно, эту же задачу можно было бы решить множеством самых разных способов. Это лишь пример удобства применения словаря defaultdict по сравнению с обычным словарем dict.

Способы создания словаря defaultdict

Во всех предыдущих примерах мы с вами использовали простейшую фабричную функцию int для формирования начального значения создаваемых ключей. Конечно, вместо нее можно прописать любую другую, например, list:

df = defaultdict(list)

Тогда при добавлении нового ключа, он будет инициализироваться пустым списком:

df['cities'] # []

Соответственно в этот список можно сразу добавлять новые значения, например:

df['coords'].extend((1, 2, 3))

В результате получим словарь со следующим содержимым:

defaultdict(<class 'list'>, {'cities': [], 'coords': [1, 2, 3]})

Класс defaultdict позволяет создавать словари с любым начальным набором ключей и значений. Например, так:

d = defaultdict(bool, x=1, y=-5.2, descr='coords')

Здесь именованные аргументы играют роль пар ключ-значение. В результате получаем следующий словарь:

defaultdict(<class 'bool'>, {'x': 1, 'y': -5.2, 'descr': 'coords'})

А при обращении к любому несуществующему ключу:

d['fl'] # False

будет формироваться значение False – как результат работы функции bool().

Давайте рассмотрим еще один полезный пример использования словаря defaultdict. Предположим, нам дан следующий список посещений людьми разных городов России:

cities = [('Alex', 'Moscow'), ('Sergey', 'Tula'), ('Masha', 'Vitebsk'),
       ('Sergey', 'Moscow'), ('Sergey', 'Tula'), ('Masha', 'Ufa'),
       ('Alex', 'Vitebsk'), ('Sergey', 'Moscow'), ('Alex', 'Moscow'),
       ]

И ставится задача сгруппировать уникальные города по каждому путешественнику. То есть, нужно сформировать словарь с ключами имен путешественников, а значениями – набором уникальных посещенных городов. Сделать это можно следующим образом:

persons = defaultdict(set)
 
for k, v in cities:
    persons[k].add(v)
 
print(persons)

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

defaultdict(<class 'set'>, {'Alex': {'Vitebsk', 'Moscow'}, 'Sergey': {'Tula', 'Moscow'}, 'Masha': {'Vitebsk', 'Ufa'}})

Видите, как удобно, легко и красиво можно решить поставленную задачу с помощью словаря defaultdict!

Использование собственных фабричных функций

Во всех предыдущих примерах мы использовали встроенные функции для формирования начальных значений ключей. Однако никто не запрещает указывать свои собственные. Например, можно объявить какую-либо функцию:

def df_init():
    return 5

И указать ее первым аргументом при создании словаря:

df = defaultdict(df_init)
x = df['x']
 
print(df) # defaultdict(<function df_init at 0x00000235878804A0>, {'x': 5})

В результате ключ ‘x’ будет инициализирован значением 5. Конечно, когда функция очень простая, то лучше применять анонимные (лямбда) функции:

df = defaultdict(lambda: 5)

Результат будет тем же. Поэтому отдельное объявление функций имеет смысл только при реализации более сложной логики, например:

def df_init(start=0):
    def counter():
        nonlocal start
        start += 1
        return start
                      
    return counter

Данная функция при своем вызове:

f = df_init()

возвращает вложенную функцию counter и переменная f ссылается уже на нее. При каждом вызове функции f() возвращаемое значение будет увеличиваться на единицу. Поэтому, создавая словарь defaultdict командой:

df = defaultdict(df_init(2))

для каждого нового ключа значение будет постоянно увеличиваться:

x = df['x']
y = df['y']
print(df) # defaultdict(<function df_init.<locals>.counter at ...>, {'x': 3, 'y': 4})

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

Курс по стандартной библиотеке: https://stepik.org/a/259466?utm_source=proproprogs

Видео по теме