Python: defaultdict из defaultdict?

323

Есть ли способ 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()
Джонатан
источник
6
Смотрите аналогичный вопрос Каков наилучший способ реализации вложенных словарей в Python? , Также есть некоторая полезная информация в статье Википедии об автовивификации .
Мартино

Ответы:

571

Да, вот так:

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().

mouad
источник
7
это прекрасно работает! Не могли бы вы объяснить, что стоит за этим синтаксисом?
Джонатан
37
@Jonathan: Да, конечно, аргумент defaultdict(в данном случае lambda : defaultdict(int)) будет вызван, когда вы попытаетесь получить доступ к ключу, который не существует, и его возвращаемое значение будет установлено как новое значение этого ключа, которое означает в в нашем случае значение d[Key_dont_exist]будет равно defaultdict(int), и если вы попытаетесь получить доступ к ключу из этого последнего defaultdict, т.е. d[Key_dont_exist][Key_dont_exist]он вернет 0, что является возвращаемым значением аргумента последнего, defaultdictт. е. int()надеюсь, что это было полезно.
mouad
25
Аргумент defaultdictдолжен быть функцией. defaultdict(int)это словарь, а lambda: defaultdict(int)функция, которая возвращает словарь.
has2k1
27
@ has2k1 Это неправильно. Аргумент defaultdict должен быть вызываемым. Лямбда - это вызываемое.
Нильс Бом
2
@RickyLevi, если ты хочешь, чтобы это работало, ты можешь просто сказать: defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
darophi
51

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

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

Начиная с Python 2.7, есть еще лучшее решение с использованием Counter :

>>> 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 - коллекции

yanjost
источник
5
Просто чтобы завершить круг здесь, вы хотели бы использовать, d = defaultdict(lambda : Counter())а не d = defaultdict(lambda : defaultdict(int))специально для решения проблемы, как первоначально поставлено.
суждение
3
@ В этом случае вы можете просто d = defaultdict(Counter())не использовать лямбду
Deb
3
@ У вас есть небольшая ошибка - удалите внутренние скобки, чтобы передать вызываемый Counterобъект вместо объекта. То есть:d = defaultdict(Counter)
Диллон Дэвис
29

Я нахожу это немного более элегантным для использования partial:

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

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

Katriel
источник
1
Частичное также лучше, чем лямбда, потому что оно может быть применено рекурсивно :) Смотрите мой ответ ниже для общего метода вложенного фабричного дефолта.
Кампи
@Campi, вам не нужно частичное для рекурсивных приложений, AFAICT
Clément
10

Для справки, можно реализовать общий вложенный 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()

Глубина определяет количество вложенных словарей перед использованием типа, определенного в default_factory. Например:

my_dict = nested_defaultdict(list, 3)
my_dict['a']['b']['c'].append('e')
Кампи
источник
Можете ли вы привести пример использования? Не работает так, как я ожидал. ndd = nested_defaultdict(dict) .... ndd['a']['b']['c']['d'] = 'e'броскиKeyError: 'b'
Дэвид Маркс
Привет, Дэвид, тебе нужно определить глубину своего словаря, в твоем примере 3 (так как ты определил, что default_factory тоже является словарем. Nested_defaultdict (dict, 3) будет работать для тебя.
Campi
Это было очень полезно, спасибо! Одна вещь, которую я заметил, это то, что это создает default_dict at depth=0, который не всегда может быть желательным, если глубина неизвестна во время вызова. Легко исправить, добавив строку if not depth: return default_factory()вверху функции, хотя, возможно, есть более элегантное решение.
Брендан
9

В предыдущих ответах говорилось, как сделать два уровня или n уровней defaultdict. В некоторых случаях вы хотите бесконечный:

def ddict():
    return defaultdict(ddict)

Использование:

>>> d = ddict()
>>> d[1]['a'][True] = 0.5
>>> d[1]['b'] = 3
>>> import pprint; pprint.pprint(d)
defaultdict(<function ddict at 0x7fcac68bf048>,
            {1: defaultdict(<function ddict at 0x7fcac68bf048>,
                            {'a': defaultdict(<function ddict at 0x7fcac68bf048>,
                                              {True: 0.5}),
                             'b': 3})})
Клеман
источник
1
Мне это нравится. Это чертовски просто, но невероятно полезно. Спасибо!
rosstex
6

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

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

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

Стивен Румбальский
источник
4
Это решение означает, что получить все d [xa] непросто, так как вам нужно проанализировать каждый ключ, чтобы увидеть, имеет ли он xa в качестве первого элемента кортежа.
Мэтью Шинкель
5
Если вы хотите вложить 3 уровня в глубину, просто определите его как 3 уровня: d = defaultdict (lambda: defaultdict (lambda: defaultdict (int)))
Мэтью Шинкель,