Произвольно вложенный словарь из кортежей

1

Учитывая словарь с кортежами в качестве ключей (и числа/скаляры как значения), что такое Pythonic способ преобразования во вложенный словарь? Зацепка состоит в том, что с ввода-вывода входные данные имеют произвольную длину.

Ниже d1, d2 и d3 демонстрируют растущую вложенность:

from itertools import product

d1 = dict(zip(product('AB', [0, 1]), range(2*2)))
d2 = dict(zip(product('AB', [0, 1], [True, False]), range(2*2*2)))
d3 = dict(zip(product('CD', [0, 1], [True, False], 'AB'), range(2*2*2*2)))

И в результате их вложенные версии будут:

# For d1
{'A': {0: 0, 1: 1}, 'B': {0: 2, 1: 3}}

# For d2
{'A': {0: {True: 0, False: 1}, 1: {True: 2, False: 3}},
 'B': {0: {True: 4, False: 5}, 1: {True: 6, False: 7}}}

# Beginning of result for d3
{
'C': {
    0: {
        True: {
            'A': 0
            'B': 1
        },
        False: {
            'A': 2,
            'B': 3
        },
    1: # ...

Мои попытки: мне нравится этот трюк для инициализации пустой структуры данных, которая дается в ряде других ответов SO:

from collections import defaultdict

def nested_dict():
    return defaultdict(nested_dict)

Но у меня проблемы с этим, потому что количество уровней неопределенно. Я мог бы использовать что-то вроде:

def nest(d: dict) -> dict:
    res = nested_dict()
    for (i, j, k), v in d.items():
        res[i][j][k] = v
    return res

Но это будет работать только для d2 потому что его ключи имеют 3 уровня (i, j, k) выше.

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

def set_arbitrary_nest(keys, value):
    """
    >>> keys = 1, 2, 3
    >>> value = 5
    result --> {1: {2: {3: 5}}}
    """

    it = iter(keys)
    last = next(it)
    res = {last: {}}
    lvl = res
    while True:
        try:
            k = next(it)
            lvl = lvl[last]
            lvl[k] = {}
            last = k
        except StopIteration:
            lvl[k] = value
            return res

>>> set_arbitrary_nest([1, 2, 3], 5)
{1: {2: {3: 5}}}
Теги:
python-3.x
dictionary

2 ответа

2
Лучший ответ

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

def nest(d: dict) -> dict:
    result = {}
    for key, value in d.items():
        target = result
        for k in key[:-1]:  # traverse all keys but the last
            target = target.setdefault(k, {})
        target[key[-1]] = value
    return result

Вы можете использовать functools.reduce() для обработки работы с прокруткой вниз:

from functools import reduce

def nest(d: dict) -> dict:
    result = {}
    traverse = lambda r, k: r.setdefault(k, {})
    for key, value in d.items():
        reduce(traverse, key[:-1], result)[key[-1]] = value
    return result

Я использовал dict.setdefault() а не опцию autodivication defaultdict(nested_dict), так как это создает обычный словарь, который не будет в дальнейшем неявно добавлять ключи, если они еще не существуют.

Демо-версия:

>>> from pprint import pprint
>>> pprint(nest(d1))
{'A': {0: 0, 1: 1}, 'B': {0: 2, 1: 3}}
>>> pprint(nest(d2))
{'A': {0: {False: 1, True: 0}, 1: {False: 3, True: 2}},
 'B': {0: {False: 5, True: 4}, 1: {False: 7, True: 6}}}
>>> pprint(nest(d3))
{'C': {0: {False: {'A': 2, 'B': 3}, True: {'A': 0, 'B': 1}},
       1: {False: {'A': 6, 'B': 7}, True: {'A': 4, 'B': 5}}},
 'D': {0: {False: {'A': 10, 'B': 11}, True: {'A': 8, 'B': 9}},
       1: {False: {'A': 14, 'B': 15}, True: {'A': 12, 'B': 13}}}}

Обратите внимание, что вышеупомянутое решение представляет собой чистый цикл O (N) (N - длина словаря ввода), тогда как групповое решение, предложенное Ajax1234, должно сортировать входные данные для работы, делая это решение O (NlogN). Это означает, что для словаря с 1000 элементами groupby() понадобится 10.000 шагов для получения результата, тогда как линейная петля O (N) займет всего 1000 шагов. Для миллиона ключей это увеличивается до 20 миллионов шагов и т.д.

Более того, рекурсия в Python... медленная, поскольку Python не может оптимизировать такие решения для итеративного подхода. Вызовы функций относительно дороги, поэтому использование рекурсии может привести к значительным затратам на производительность, поскольку вы значительно увеличиваете количество вызовов функций и операций с дополнительными фреймами.

Временное испытание показывает, насколько это важно; используя ваши образцы d3 и 100k, мы легко видим разницу в скорости 5x:

>>> from timeit import timeit
>>> timeit('n(d)', 'from __main__ import create_nested_dict as n, d3; d=d3.items()', number=100_000)
8.210276518017054
>>> timeit('n(d)', 'from __main__ import nest as n, d3 as d', number=100_000)
1.6089267160277814
0

Вы можете использовать itertools.groupby с рекурсией:

from itertools import groupby
def create_nested_dict(d):
  _c = [[a, [(c, d) for (_, *c), d in b]] for a, b in groupby(sorted(d, key=lambda x:x[0][0]), key=lambda x:x[0][0])]
  return {a:b[0][-1] if not any([c for c, _ in b]) else create_nested_dict(b) for a, b in _c}

from itertools import product

d1 = dict(zip(product('AB', [0, 1]), range(2*2)))
d2 = dict(zip(product('AB', [0, 1], [True, False]), range(2*2*2)))
d3 = dict(zip(product('CD', [0, 1], [True, False], 'AB'), range(2*2*2*2)))
print(create_nested_dict(d1.items()))
print(create_nested_dict(d2.items()))
print(create_nested_dict(d3.items())) 

Выход:

{'A': {0: 0, 1: 1}, 'B': {0: 2, 1: 3}}
{'A': {0: {False: 1, True: 0}, 1: {False: 3, True: 2}}, 'B': {0: {False: 5, True: 4}, 1: {False: 7, True: 6}}}
{'C': {0: {False: {'A': 2, 'B': 3}, True: {'A': 0, 'B': 1}}, 1: {False: {'A': 6, 'B': 7}, True: {'A': 4, 'B': 5}}}, 'D': {0: {False: {'A': 10, 'B': 11}, True: {'A': 8, 'B': 9}}, 1: {False: {'A': 14, 'B': 15}, True: {'A': 12, 'B': 13}}}}
  • 0
    Использование groupby() здесь дорого , поскольку входные данные гарантированно требуют сортировки! Сортировка добавляет стоимость O (NlogN), излишнее, если бы сработал простой цикл O (N)! Более того, размещение всего этого на одной линии действительно очень дорого.
  • 0
    @MartijnPieters Правда, однако, отсортированный ввод необходим для itertools.groupby . Однако я думаю, что itertools.groupby делает рекурсивную логику немного проще и, возможно, чище.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню