【问题标题】:Python Recursive Search of Dict with Nested Keys带有嵌套键的字典的 Python 递归搜索
【发布时间】:2016-08-16 22:28:54
【问题描述】:

我最近不得不使用嵌套的 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'])

我将如何构建更好的解决方案?

【问题讨论】:

  • 感谢大家的解决方案!你的许多解决方案都比我的好。我的限制是我需要它可重用于许多不同的数据结构和类型。我收到的数据转储包含这种情况的几个变体,我需要能够解决所有这些问题。
  • 我不明白你为什么需要这些副本?

标签: python list dictionary recursion global


【解决方案1】:

编辑:第一个版本有一个错误,现已更正

我相信这应该可行,我们正在使用递归的力量!

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

【讨论】:

  • strip_leaves_from_tree(d) 返回[{'stuff': 's1'}]
  • 已修复,发帖前应该已经测试过了。
【解决方案2】:

这是一个稍微修改过的版本,没有使用全局变量。将h 设置为None 默认情况下并为第一次调用_get_recursive_results() 创建一个新列表。稍后在对_get_recursive_results() 的递归调用中提供h 作为参数:

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

【讨论】:

  • 谢谢!这确实很有帮助!我无法接受其他解决方案,因为我无法控制初始 dict 的结构 - 而且每次数据转储都会收到许多不同的类型。
  • 我仍然不确定如果没有对其进行任何修改,为什么在 for 循环中需要多次克隆 dict?
  • 是的,dict的副本真的没必要。
【解决方案3】:

我验证它有效。请检查一下。 当然,当你改变dictionary-list的结构时,它应该被修改。

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)

【讨论】:

    【解决方案4】:

    这不是通用的,但可以完成工作:

    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'}]
    

    【讨论】:

      【解决方案5】:

      使用生成器

      使用以下生成器:

      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的一些方法

      发电机

      通常,如果您需要建立一个列表并搜索替换全局变量,生成器可能 派上用场,因为它们将当前工作的状态保存在其局部变量中,而且构建整个结果被推迟到使用生成的值。

      递归

      递归将子结果存储在堆栈中的局部变量中。

      具有内部属性的类实例

      一个类可以作为一个罐子来封装你的变量。

      您将中间结果存储在实例属性中,而不是使用全局变量。

      针对不同的数据结构进行泛化

      在您提到的 cmets 中,每次转储都会收到许多不同的类型。

      我会假设您的数据符合以下预期:

      • 具有树状结构
      • 树中的每个节点都应有助于产生一些结果(例如字典{"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_valueget_subitems - 根据您的喜好使用相同或不同的名称。

      【讨论】:

      • @ok123jump 我修改了代码以允许动态键名。
      • @ok123jump 添加了泛化功能,允许自定义任何树状数据结构的代码(我回答的最后一部分)。
      【解决方案6】:

      【讨论】:

        猜你喜欢
        • 2018-09-28
        • 2011-12-02
        • 2012-07-26
        • 1970-01-01
        • 2014-03-06
        • 1970-01-01
        • 2021-12-04
        • 1970-01-01
        • 2023-02-10
        相关资源
        最近更新 更多