Вложенный defaultdict из defaultdict

Есть ли способ сделать defaultdict также по умолчанию для defaultdict? (т.е. рекурсивный defaultdict с бесконечным уровнем?)

Я хочу иметь возможность:

x = defaultdict(...stuff...)
x[0][1][0]
{}

Итак, я могу сделать x = defaultdict(defaultdict), но это только второй уровень:

x[0]
{}
x[0][0]
KeyError: 0

Есть рецепты, которые позволяют это сделать. Но можно ли это сделать, просто используя обычные аргументы defaultdict?

Обратите внимание, что здесь спрашивается, как сделать рекурсивный defaultdict бесконечного уровня, поэтому он отличается от Python: defaultdict of defaultdict?< /em>, как сделать двухуровневый defaultdict.

Я, вероятно, просто закончу тем, что использую шаблон bunch, но когда я понял, что не знаю, как это сделать, это меня заинтересовало.


person Corley Brigman    schedule 04.10.2013    source источник
comment
Возможный дубликат Python: defaultdict of defaultdict?   -  person malioboro    schedule 13.01.2017
comment
Не совсем ... добавил информацию к вопросу, чтобы указать, почему. Хотя это полезный вопрос.   -  person Corley Brigman    schedule 13.01.2017


Ответы (9)


Для произвольного количества уровней:

def rec_dd():
    return defaultdict(rec_dd)

>>> x = rec_dd()
>>> x['a']['b']['c']['d']
defaultdict(<function rec_dd at 0x7f0dcef81500>, {})
>>> print json.dumps(x)
{"a": {"b": {"c": {"d": {}}}}}

Конечно, вы также можете сделать это с помощью лямбды, но я считаю, что лямбды менее читабельны. В любом случае это будет выглядеть так:

rec_dd = lambda: defaultdict(rec_dd)
person Andrew Clark    schedule 04.10.2013
comment
Действительно отличный пример, спасибо. Не могли бы вы расширить его до случая, когда данные загружаются из json в defaultdict из defaultdict? - person David Belohrad; 04.03.2017
comment
Одна нота. Если вы пытаетесь использовать этот код во время травления, lambda не сработает. - person Viacheslav Kondratiuk; 03.04.2017
comment
Как мы можем определить тип последнего значения в нижней части словаря? - person Diamond; 28.06.2021
comment
отличное решение! - person rok; 24.07.2021

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

Возможно, вы искали:

defaultdict(lambda: defaultdict(dict))

Причины, по которым вы можете предпочесть эту конструкцию:

  • Это более явное решение, чем рекурсивное, и поэтому, вероятно, более понятное для читателя.
  • Это позволяет «листу» defaultdict быть чем-то отличным от словаря, например: defaultdict(lambda: defaultdict(list)) или defaultdict(lambda: defaultdict(set))
person Chris W.    schedule 07.01.2015
comment
defaultdict(lambda: defaultdict(list)) Правильная форма? - person Yuvaraj Loganathan; 09.02.2015
comment
Упс, да, форма lambda верна, потому что defaultdict(something) возвращает объект, похожий на словарь, а defaultdict ожидает callable! Спасибо! - person Chris W.; 12.02.2015
comment
Это было отмечено как возможный дубликат другого вопроса... но это был не мой первоначальный вопрос. Я знал, как создать двухуровневый defaultdict; чего я не знал, так это как сделать его рекурсивным. Этот ответ на самом деле похож на stackoverflow.com/questions/5029934 / - person Corley Brigman; 13.01.2017
comment
Одним из недостатков лямбда-подхода является то, что генерируемые им объекты не могут быть промаринованы... но вы можете обойти это, приведя к обычному dict(result) перед рассолом - person CpILL; 16.03.2020
comment
Кажется, это работает только для уровней рекурсии выше 1. Например, nested_dict['foo']['bar'].append('baz') работает, но nested_dict['foo'].append('bar') не работает, потому что класс defaultdict не имеет атрибута append - person Addison Klinke; 23.02.2021

Для этого есть хитрый трюк:

tree = lambda: defaultdict(tree)

Затем вы можете создать свой x с x = tree().

person BrenBarn    schedule 04.10.2013

Подобно решению BrenBarn, но не содержит дважды имя переменной tree, поэтому оно работает даже после изменений в словаре переменных:

tree = (lambda f: f(f))(lambda a: (lambda: defaultdict(a(a))))

Затем вы можете создавать каждый новый x с x = tree().


Для версии def мы можем использовать область закрытия функции, чтобы защитить структуру данных от недостатка, из-за которого существующие экземпляры перестают работать, если имя tree восстанавливается. Это выглядит так:

from collections import defaultdict

def tree():
    def the_tree():
        return defaultdict(the_tree)
    return the_tree()
person pts    schedule 04.10.2013
comment
мне придется подумать об этом (это немного сложнее). но я думаю, что ваша точка зрения заключается в том, что если сделать x = tree(), но потом кто-то приходит позже и делает tree=None, это все равно будет работать, а это не будет? - person Corley Brigman; 05.10.2013

Я бы также предложил реализацию в стиле ООП, которая поддерживает бесконечное вложение, а также правильно отформатированное repr.

class NestedDefaultDict(defaultdict):
    def __init__(self, *args, **kwargs):
        super(NestedDefaultDict, self).__init__(NestedDefaultDict, *args, **kwargs)

    def __repr__(self):
        return repr(dict(self))

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

my_dict = NestedDefaultDict()
my_dict['a']['b'] = 1
my_dict['a']['c']['d'] = 2
my_dict['b']

print(my_dict)  # {'a': {'b': 1, 'c': {'d': 2}}, 'b': {}}
person Stanislav Tsepa    schedule 28.05.2019
comment
Аккуратный ! Я добавил сквозную передачу *args и **kwargs, которая позволяет ему работать как defaultdict, а именно создавать словарь с аргументами ключевого слова. Это полезно для передачи NestedDefaultDict в json.load - person Ciprian Tomoiagă; 06.08.2019
comment
Попытка my_dict = NestedDefaultDict(list) возвращает TypeError - предназначен ли *args для разрешения определения типа листа таким образом? - person Addison Klinke; 23.02.2021
comment
@AddisonKlinke Нет, в этой реализации это не так. Аргумент default_factory уже занят типом NestedDefaultDict. Нет простого способа проверить, является ли текущий узел листовым или нет, без создания более сложного класса. Однако вы можете написать что-то вроде my_dict['a']['b'][0], чтобы имитировать узел со списком. - person Stanislav Tsepa; 24.02.2021

Я основывал это на ответе Эндрю. Если вы хотите загрузить данные из json или существующего словаря в нестерский defaultdict, см. этот пример:

def nested_defaultdict(existing=None, **kwargs):
    if existing is None:
        existing = {}
    if not isinstance(existing, dict):
        return existing
    existing = {key: nested_defaultdict(val) for key, val in existing.items()}
    return defaultdict(nested_defaultdict, existing, **kwargs)

https://gist.github.com/nucklehead/2d29628bb49115f3c30e78c071207775

person nucklehead    schedule 17.06.2020

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

def defdict_to_dict(defdict, finaldict):
    # pass in an empty dict for finaldict
    for k, v in defdict.items():
        if isinstance(v, defaultdict):
            # new level created and that is the new value
            finaldict[k] = defdict_to_dict(v, {})
        else:
            finaldict[k] = v
    return finaldict

defdict_to_dict(my_rec_default_dict, {})
person Dr. XD    schedule 10.04.2020

ответ @nucklehead также можно расширить для обработки массивов в JSON:

def nested_dict(existing=None, **kwargs):
    if existing is None:
        existing = defaultdict()
    if isinstance(existing, list):
        existing = [nested_dict(val) for val in existing]
    if not isinstance(existing, dict):
        return existing
    existing = {key: nested_dict(val) for key, val in existing.items()}
    return defaultdict(nested_dict, existing, **kwargs)
person Josh Olson    schedule 15.10.2020

Вот функция для произвольного базового defaultdict для произвольной глубины вложенности.

(перекрестная публикация из Невозможно рассолить defaultdict)

def wrap_defaultdict(instance, times=1):
    """Wrap an instance an arbitrary number of `times` to create nested defaultdict.
    
    Parameters
    ----------
    instance - list, dict, int, collections.Counter
    times - the number of nested keys above `instance`; if `times=3` dd[one][two][three] = instance
    
    Notes
    -----
    using `x.copy` allows pickling (loading to ipyparallel cluster or pkldump)
        - thanks https://stackoverflow.com/questions/16439301/cant-pickle-defaultdict
    """
    from collections import defaultdict

    def _dd(x):
        return defaultdict(x.copy)

    dd = defaultdict(instance)
    for i in range(times-1):
        dd = _dd(dd)

    return dd
person BML    schedule 15.07.2021