defaultdict defaultdict, вложенный

Есть ли способ сделать defaultdict также по умолчанию для defaultdict?

IOW, если я сделаю:

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

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

Итак, я могу сделать:

x = defaultdict(defaultdict)

Но это только один уровень:

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

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

Обратите внимание, что кто-то пометил это как копию Python: defaultdict из defaultdict?, но это не тот вопрос... этот вопрос заключался в том, как сделать двухуровневый дефолт; это как сделать рекурсивный дефолт бесконечного уровня.

12 ответов

Решение

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

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)

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

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

defaultdict(lambda: defaultdict(dict))

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

  • Оно более явное, чем рекурсивное решение, и, следовательно, вероятно, более понятное для читателя.
  • Это позволяет "лист" defaultdict быть чем-то отличным от словаря, например: defaultdict(lambda: defaultdict(list)) или же defaultdict(lambda: defaultdict(set))

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

tree = lambda: defaultdict(tree)

Тогда вы можете создать свой x с x = tree(),

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

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

    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': {}}

Похож на решение 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()

Однако, основываясь на ответе Криса В., чтобы решить проблему аннотации типа, вы можете сделать ее фабричной функцией, которая определяет подробные типы. Например, это окончательное решение моей проблемы, когда я исследовал этот вопрос:

      def frequency_map_factory() -> dict[str, dict[str, int]]:
    """
    Provides a recorder of: per X:str, frequency of Y:str occurrences.
    """
    return defaultdict(lambda: defaultdict(int))

Я основал это на ответе Эндрю здесь. Если вы хотите загрузить данные из json или существующего dict в nester 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

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

(перекрестная публикация из Can't pickle 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

Вот решение, похожее на ответ @Stanislav, которое работает с многопроцессорностью, а также позволяет завершить вложение:

      from collections import defaultdict
from functools import partial

class NestedDD(defaultdict):
    def __init__(self, n, *args, **kwargs):
        self.n = n
        factory = partial(build_nested_dd, n=n - 1) if n > 1 else int
        super().__init__(factory, *args, **kwargs)

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

def build_nested_dd(n):
    return NestedDD(n)

Вот решение, похожее на @Chris W., которое делает возможным больше уровней. Он по-прежнему позволяет указывать «лист» как нечто большее, чем defaultdict.

Вместо лямбды определяется замыкание.

Возможно, вы предпочтете этот метод, потому что

  • объявление вложенного defaultdict написано как вложенные функции, поэтому его легче читать.
  • Возможно более двух уровней.
  • Последний лист может быть: list, set,...

Вот пример.

      from collections import defaultdict
import json

def another_defaultdict(factory):
    'return another layer of defaultdict as a factory function'
    def layer():
        return defaultdict(factory)  
    return layer




>>> # two levels
>>> d = defaultdict(another_defaultdict(list))

>>> # three levels
>>> d = defaultdict(another_defaultdict(another_defaultdict(list)))


>>> d['Canada']['Alberta'] = ['Calgary', 'Magrath', 'Cardston', 'Lethbridge']
>>> d['France']['Nord'] = ['Dunkirk', 'Croix']
>>> print(json.dumps(d, indent=2))
{
  "Canada": {
    "Alberta": [
      "Calgary",
      "Magrath",
      "Cardston",
      "Lethbridge"
    ]
  },
  "France": {
    "Nord": [
      "Dunkirk",
      "Croix"
    ]
  }
}

@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)

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

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, {})
Другие вопросы по тегам