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, {})