Рекурсивный поиск 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
Это хороший способ поиска по дикт
