Рекурсивный поиск Dict на Python с вложенными ключами

недавно мне пришлось решить проблему в реальной системе данных с вложенной комбинацией dict/list. Я работал над этим довольно долго и придумал решение, но я очень недоволен. Мне пришлось прибегнуть к помощи globals() и именованный временный глобальный параметр.

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

проблема Набор данных:

d = {
    "k":1,
    "stuff":"s1",
    "l":{"m":[
        {
            "k":2,
            "stuff":"s2",
            "l":None
        },
        {
            "k":3,
            "stuff":"s3",
            "l":{"m":[
                {
                    "k":4,
                    "stuff":"s4",
                    "l":None
                },
                {
                    "k":5,
                    "stuff":"s5",
                    "l":{"m":[
                        {
                            "k":6,
                            "stuff":"s6",
                            "l":None
                        },
                    ]}
                },
            ]}
        },
    ]}
}

Желаемый Результат:

[{'k': 1, 'stuff': 's1'},
 {'k': 2, 'stuff': 's2'},
 {'k': 3, 'stuff': 's3'},
 {'k': 4, 'stuff': 's4'},
 {'k': 5, 'stuff': 's5'},
 {'k': 6, 'stuff': 's6'}]

Мое Решение:

def _get_recursive_results(d, iter_key, get_keys):
    if not 'h' in globals():
        global h
        h = []
    h.append({k:d.get(k) for k in get_keys})

    d2 = d.copy()
    for k in iter_key:
        if not d2:
            continue
        d2 = d2.get(k)

    for td in d2:
        d3 = td.copy()
        for k in iter_key:
            if not d3:
                continue
            d3 = d3.get(k)

        if d3:
            return _get_recursive_results(td, iter_key, get_keys)
        h.append({k:td.get(k) for k in get_keys})
    else:
        l = [k for k in h]
        del globals()['h']
        return l

вызов моей функции следующим образом возвращает желаемый результат:

_get_recursively(d, ['l','m'], ['k','stuff'])

как бы я построил лучшее решение?

6 ответов


это слегка измененная версия без использования глобальных переменных. Set h to None по умолчанию и создать новый список для первого вызова _get_recursive_results(). Позже предоставить h в качестве аргумента в рекурсивных вызовов _get_recursive_results():

def _get_recursive_results(d, iter_key, get_keys, h=None):
    if h is None:
        h = []
    h.append({k:d.get(k) for k in get_keys})
    d2 = d.copy()
    for k in iter_key:
        if not d2:
            continue
        d2 = d2.get(k)
    for td in d2:
        d3 = td.copy()
        for k in iter_key:
            if not d3:
                continue
            d3 = d3.get(k)
        if d3:
            return _get_recursive_results(td, iter_key, get_keys, h)
        h.append({k:td.get(k) for k in get_keys})
    else:
        l = [k for k in h]
        return l

теперь:

>>> _get_recursive_results(d, ['l','m'], ['k','stuff'])
[{'k': 1, 'stuff': 's1'},
 {'k': 2, 'stuff': 's2'},
 {'k': 3, 'stuff': 's3'},
 {'k': 4, 'stuff': 's4'},
 {'k': 5, 'stuff': 's5'},
 {'k': 6, 'stuff': 's6'}]

нет необходимости в копировании промежуточных диктов. Это еще одна модифицированная версия без копирования:

def _get_recursive_results(d, iter_key, get_keys, h=None):
    if h is None:
        h = []
    h.append({k: d.get(k) for k in get_keys})
    for k in iter_key:
        if not d:
            continue
        d = d.get(k)
    for td in d:
        d3 = td
        for k in iter_key:
            if not d3:
                continue
            d3 = d3.get(k)
        if d3:
            return _get_recursive_results(td, iter_key, get_keys, h)
        h.append({k: td.get(k) for k in get_keys})
    else:
        return h

Это не так универсально, но это делает работу:

def parse_tree(d, keys):
   result = [{key: d[key] for key in keys}]
   l = d.get('l', None)
   if l is not None:
       entries = l.get('m', [])
       for entry in entries:
           result.extend(parse_tree(entry))
   return result


>>> parse_tree(d, ['k', 'stuff'])
[{'k': 1, 'stuff': 's1'},
 {'k': 2, 'stuff': 's2'},
 {'k': 3, 'stuff': 's3'},
 {'k': 4, 'stuff': 's4'},
 {'k': 5, 'stuff': 's5'},
 {'k': 6, 'stuff': 's6'}]

использовать генератор

при следующих генераторов:

def get_stuff(dct, iter_keys, get_keys):
    k, stuff = get_keys
    l, m = iter_keys
    if k in dct:
        yield {k: dct[k], stuff: dct[stuff]}
        if dct.get(l):
            for subdct in dct[l][m]:
                for res in get_stuff(subdct, iter_keys, get_keys):
                    yield res


list(get_stuff(d, ["l", "m"], ["k", "stuff"]))

вы получили результаты на:

list(get_stuff(d))

Python 3.3 предоставляет новые yield from выражение, используемое для делегирования уступки подгенератору. Используя это выражение, код может быть на одну строку короче:

def get_stuff(dct):
    if "k" in dct:
        yield {"k": dct["k"], "stuff": dct["stuff"]}
        if dct.get("l"):
            for subdct in dct["l"]["m"]:
                yield from get_stuff(subdct)

def get_stuff(dct, iter_keys, get_keys):
    k, stuff = get_keys
    l, m = iter_keys
    if k in dct:
        yield {k: dct[k], stuff: dct[stuff]}
        if dct.get(l):
            for subdct in dct[l][m]:
                yield from get_stuff(subdct, iter_keys, get_keys):

некоторые методы, чтобы избежать globals

генераторы

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

рекурсия

рекурсия хранить subresults в локальных переменных в стеке.

экземпляр класса с внутренним собственность

класс может служить tin для инкапсуляции ваших переменных.

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

обобщить для различных структур данных

в ваших комментариях вы упомянули, что вы получаете много разных типов с каждым дампом.

я буду считать, что ваши данные соответствуют следующим ожиданиям:

  • имеет древовидную структуру
  • каждый узел в дереве должен внести свой вклад в результат некоторого результата (например, словарь {"k": xx, "stuff": yy})
  • каждый узел может содержать подэлементы (список подузлы)

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

здесь я использую get_value для доставки значения узла и get_subitems для доставки подузлов:

def get_value(data):
    try:
        return {"k": data["k"], "stuff": data["stuff"]}
    except KeyError:
        return None


def get_subitems(data):
    try:
        return data["l"]["m"]
    except TypeError:
        return None

обработка затем выполняется:

def get_stuff(dct, get_value_fun, get_subitems_fun):
    value = get_value(dct)
    if value:
        yield value
        lst = get_subitems_fun(dct)
        if lst:
            for subdct in lst:
                for res in get_stuff(subdct, get_value_fun, get_subitems_fun):
                    yield res

называют таким образом:

get_stuff(d, get_value, get_subitems)

преимущество использование функций заключается в том, что он гораздо более гибок для любых данных структуры, которые вам придется обрабатывать (адаптация к другим структурам данных потребует только предоставления настроенной версии функции get_value и get_subitems - с одинаковыми или разными именами в соответствии с вашими предпочтениями.


Edit: в первой версии была ошибка, которая теперь исправлена

Я считаю, что это должно работать, мы используем силу рекурсия!

def strip_leaves_from_tree(my_tree):
    result = list()
    row = dict()
    for key in my_tree:
        child = my_tree[key]
        if type(child) in (int, str,):
            row[key] = child
        elif isinstance(child, dict):
            result = strip_leaves_from_tree(child)
        elif isinstance(child, list):
            for element in child:
                result += strip_leaves_from_tree(element)
    if row: result = [row,]+result
    return result

Я проверил, что он работает. Пожалуйста, проверьте. Конечно, он должен быть изменен при изменении структуры словаря-список.

def add(ret, val):
  if val is not None: ret.append(val)

def flatten(d, ret):
  for k,v in d.items():
    if isinstance(v, dict): add(ret,flatten(v, ret))
    elif isinstance(v, list):
        for i in v: add(ret, flatten(i, ret))
    elif k=='k':
        ret.append({'k':v,'stuff':d.get('stuff')})

ret = []
flatten(d, ret)

взгляните на https://github.com/akesterson/dpath-python/blob/master/README.rst

Это хороший способ поиска по дикт