Python: defaultdict defaultdict?

Есть ли способ иметь defaultdict(defaultdict(int)), чтобы заставить работать следующий код?

for x in stuff:
    d[x.a][x.b] += x.c_int

d необходимо создать ad-hoc, в зависимости от элементов x.a и x.b.

Я мог бы использовать:

for x in stuff:
    d[x.a,x.b] += x.c_int

но тогда я не смог бы использовать:

d.keys()
d[x.a].keys()

Ответ 1

Да вот так:

defaultdict(lambda: defaultdict(int))

Аргумент a defaultdict (в данном случае lambda: defaultdict(int)) будет вызываться при попытке получить доступ к ключу, который не существует. Возвращаемое значение будет установлено как новое значение этого ключа, что означает, что в нашем случае значение d[Key_doesnt_exist] будет defaultdict(int).

Если вы попытаетесь получить доступ к ключу из этого последнего defaultdict, то есть d[Key_doesnt_exist][Key_doesnt_exist], он вернет 0, что является возвращаемым значением аргумента последнего значения defaultdict, т.е. int().

Ответ 2

Параметр конструктора defaultdict - это функция, которая будет вызываться для создания новых элементов. Поэтому позвольте использовать лямбда!

>>> from collections import defaultdict
>>> d = defaultdict(lambda : defaultdict(int))
>>> print d[0]
defaultdict(<type 'int'>, {})
>>> print d[0]["x"]
0

С Python 2.7 существует еще лучшее решение с использованием счетчика:

>>> from collections import Counter
>>> c = Counter()
>>> c["goodbye"]+=1
>>> c["and thank you"]=42
>>> c["for the fish"]-=5
>>> c
Counter({'and thank you': 42, 'goodbye': 1, 'for the fish': -5})

Некоторые бонусные функции

>>> c.most_common()[:2]
[('and thank you', 42), ('goodbye', 1)]

Подробнее см. PyMOTW - Коллекции - Типы данных контейнера и Python Документация - коллекции

Ответ 3

Я считаю несколько более элегантным использовать partial:

import functools
dd_int = functools.partial(defaultdict, int)
defaultdict(dd_int)

Конечно, это то же самое, что и лямбда.

Ответ 4

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

for x in stuff:
    d[x.a][x.b] += x.c_int

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

d = defaultdict(int)
for x in stuff:
    d[x.a,x.b] += x.c_int
    # ^^^^^^^ tuple key

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

Ответ 5

Для справки, можно реализовать общий вложенный defaultdict фабрики defaultdict через:

from collections import defaultdict
from functools import partial
from itertools import repeat


def nested_defaultdict(default_factory, depth=1):
    result = partial(defaultdict, default_factory)
    for _ in repeat(None, depth - 1):
        result = partial(defaultdict, result)
    return result()