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

Если мы пытаемся подсчитать слова в документе, общий подход заключается в создании словаря, в котором словарь keys содержит слова, а словарь values содержит количество этих слов.

Попробуем сделать это с помощью обычного словаря.

Во-первых, для настройки мы возьмем список слов и split() в отдельные слова. Я взял этот абзац из другого проекта, над которым я работаю, и искусственно добавил несколько лишних слов, чтобы определенные слова появлялись более одного раза (скоро станет ясно почему).

# paragraph
lines = ["This table highlights 538's new NBA statistic, RAPTOR, in addition to the more established Wins Above Replacement (WAR). An extra column, Playoff (P/O) War, is provided to highlight stars performers in the post-season, when the stakes are higher. The table is limited to the top-100 players who have played at least 1,000 minutes minutes the table Wins NBA NBA RAPTOR more players"]
# split paragraphy into individual words
lines = " ".join(lines).split()

# list
type(lines) 

Теперь, когда у нас есть список lines, мы создадим пустой dict с именем word_counts, и пусть каждое слово будет key, а каждое value будет количеством этого слова.

# empty list
word_counts = {}
# loop through lines to count each word
for word in lines:
    word_counts[word] += 1
# KeyError: 'This'

Мы получили KeyError за самое первое слово в lines (т. е. «Это»), потому что список пытался подсчитать несуществующий ключ. Мы научились обрабатывать исключения, поэтому можем использовать try и except.

Здесь мы перебираем lines, и когда мы пытаемся подсчитать несуществующий ключ, как мы делали это раньше, мы сейчас ожидаем KeyError и установим начальный счетчик равным 1, затем он может продолжить цикл и подсчитывать слово, которое теперь существует, чтобы его можно было увеличить.

# empty list
word_counts = {}
# exception handling
for word in lines:
    try:
        word_counts[word] += 1
    except KeyError:
        word_counts[word] = 1
# call word_counts
# abbreviated for space
word_counts
{'This': 1,
 'table': 3,
 'highlights': 1,
 "538's": 1,
 'new': 1,
 'NBA': 3,
 'statistic,': 1,
 'RAPTOR,': 1,
 'in': 2,
 'addition': 1,
 'to': 3,
 'the': 5,
 'more': 2,
 ...
 'top-100': 1,
 'players': 2,
 'who': 1,
 'have': 1,
 'played': 1,
 'at': 1,
 'least': 1,
 '1,000': 1,
 'minutes': 2,
 'RAPTOR': 1}

Теперь есть и другие способы добиться вышеперечисленного:

# use conditional flow
word_counts = {}
for word in lines:
    if word in word_counts:
        word_counts[word] += 1
    else:
        word_counts[word] = 1
# use get
for word in lines:
    previous_count = word_counts.get(word, 0)
    word_counts[word] = previous_count + 1

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

defaultdict является подклассом dict и должен быть импортирован из collections:

from collections import defaultdict
word_counts = defaultdict(int)
for word in lines:
    word_counts[word] += 1
# we no longer get a KeyError
# abbreviated for space
defaultdict(int,
            {'This': 1,
             'table': 3,
             'highlights': 1,
             "538's": 1,
             'new': 1,
             'NBA': 3,
             'statistic,': 1,
             'RAPTOR,': 1,
             'in': 2,
             'addition': 1,
             'to': 3,
             'the': 5,
             'more': 2,
             ...
             'top-100': 1,
             'players': 2,
             'who': 1,
             'have': 1,
             'played': 1,
             'at': 1,
             'least': 1,
             '1,000': 1,
             'minutes': 2,
             'RAPTOR': 1})

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

Если вы хотите, чтобы ваш defaultdict имел values значение lists, вы можете передать list в качестве аргумента. Затем, когда вы append значение, оно автоматически содержится в list.

dd_list = defaultdict(list) # defaultdict(list, {})
dd_list[2].append(1)        # defaultdict(list, {2: [1]})
dd_list[4].append('string') # defaultdict(list, {2: [1], 4: ['string']})

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

dd_dict = defaultdict(dict) # defaultdict(dict, {})
# match key-with-value
dd_dict['first_name'] = 'lebron' # defaultdict(dict, {'first_name': 'lebron'})
dd_dict['last_name'] = 'james'   
# match key with dictionary containing another key-value pair
dd_dict['team']['city'] = 'Los Angeles'
# defaultdict(dict,
#            {'first_name': 'lebron',
#             'last_name': 'james',
#             'team': {'city': 'Los Angeles'}})

Применение: группировка с помощью defaultdict

Следующий пример взят из Real Python, фантастического ресурса для всего, что связано с Python.

Обычно используется defaultdict для группировки элементов в последовательности или коллекции, устанавливая начальный параметр (он же .default_factory) равным list.

dep = [('Sales', 'John Doe'),
       ('Sales', 'Martin Smith'),
       ('Accounting', 'Jane Doe'),
       ('Marketing', 'Elizabeth Smith'),
       ('Marketing', 'Adam Doe')]
from collections import defaultdict
dep_dd = defaultdict(list)
for department, employee in dep:
    dep_dd[department].append(employee)
dep_dd
#defaultdict(list,
#            {'Sales': ['John Doe', 'Martin Smith'],
#             'Accounting': ['Jane Doe'],
#             'Marketing': ['Elizabeth Smith', 'Adam Doe']})

Что произойдет, если у вас есть повторяющиеся записи? Мы немного забегаем вперед, чтобы использовать дубликаты дескрипторов set и группировать только уникальные записи:

# departments with duplicate entries
dep = [('Sales', 'John Doe'),
       ('Sales', 'Martin Smith'),
       ('Accounting', 'Jane Doe'),
       ('Marketing', 'Elizabeth Smith'),
       ('Marketing', 'Elizabeth Smith'),
       ('Marketing', 'Adam Doe'),
       ('Marketing', 'Adam Doe'),
       ('Marketing', 'Adam Doe')]
# use defaultdict with set
dep_dd = defaultdict(set)
# set object has no attribute 'append'
# so use 'add' to achieve the same effect
for department, employee in dep:
    dep_dd[department].add(employee)
dep_dd
#defaultdict(set,
#            {'Sales': {'John Doe', 'Martin Smith'},
#             'Accounting': {'Jane Doe'},
#             'Marketing': {'Adam Doe', 'Elizabeth Smith'}})

Применение: накопление с помощью defaultdict

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

incomes = [('Books', 1250.00),
           ('Books', 1300.00),
           ('Books', 1420.00),
           ('Tutorials', 560.00),
           ('Tutorials', 630.00),
           ('Tutorials', 750.00),
           ('Courses', 2500.00),
           ('Courses', 2430.00),
           ('Courses', 2750.00),]
# enter float as argument        
dd = defaultdict(float)  # collections.defaultdict
for product, income in incomes:
    dd[product] += income
# defaultdict(float, {'Books': 3970.0, 'Tutorials': 1940.0, 'Courses': 7680.0})

for product, income in dd.items():
    print(f"Total income for {product}: ${income:,.2f}")
# Total income for Books: $3,970.00
# Total income for Tutorials: $1,940.00
# Total income for Courses: $7,680.00

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

Таким образом, dictionaries и defaultdict можно использовать для группировки элементов, накопления элементов и подсчета элементов. Оба могут быть использованы, даже если key (еще) не существует, но его defaultdict обрабатывает это более лаконично. А пока остановимся на этом и перейдем к следующей теме: счетчики.

***

Чтобы узнать больше о науке о данных, машинном обучении, R, Python, SQL и многом другом, найдите меня в Twitter.