如何以 pythonic 方式通过嵌套字典按键过滤

How to filter by keys through a nested dictionary in a pythonic way

尝试过滤嵌套字典。我的解决方案很笨拙,希望看看是否有更好的方法使用理解。只对这个例子的字典和列表感兴趣。

_dict_key_filter() 将过滤嵌套字典或嵌套字典列表的键。任何不在 obj_filter 中的内容都将在所有嵌套级别上被忽略。

obj : 可以是字典或字典列表。

obj_filter:必须是过滤值列表

def _dict_key_filter(self, obj, obj_filter):
    if isinstance(obj, dict):
        retdict = {}
        for key, value in obj.iteritems():
            if key in obj_filter:
                retdict[key] = copy.deepcopy(value)
            elif isinstance(value, (dict, list)):
                child = self._dict_key_filter(value, obj_filter)
                if child:
                    retdict[key] = child
        return retdict if retdict else None
    elif isinstance(obj, list):
        retlist = []
        for value in list:
            child = self._dict_key_filter(value, obj_filter)
            if child:
                retlist.append(child)
        return retlist if retlist else None
    else:
        return None

Example#
dict1 = {'test1': {'test2':[1,2]}, 'test3': [{'test6': 2}, 
         {'test8': {'test9': 23}}], 'test4':{'test5': 5}}

filter = ['test5' , 'test9']

return = _dict_key_filter(dict1, filter)

return value would be {'test3': [{'test8': {'test9': 23}}], 'test4': {'test5': 5}}

这是一个非常古老的问题。我最近遇到了类似的问题。

这可能很明显,但您正在处理一棵树,其中每个节点都有任意数量的子节点。您想要将不包含某些项目的子树切割为节点(不是叶子)。为此,您正在使用自定义 DFS:主函数 return 是一个子树或 None。如果值为 None 那么您 "cut" 分支。

首先,函数dict_key_filter returns a(非空)dict,a(非空)list or None if no在分支中找不到过滤键。 为了降低复杂性,您可以在每种情况下 return a sequence:如果没有找到过滤键,则为空序列,如果您仍在搜索或找到树的叶子,则为非空序列。您的代码如下所示:

def dict_key_filter(obj, obj_filter):
    if isinstance(obj, dict):
        retdict = {}
        ...
        return retdict # empty or not
    elif isinstance(obj, list):
        retlist = []
        ...
        return retlist # empty or not
    else:
        return [] # obvioulsy empty

这是简单的部分。现在我们必须填充点。

list案例

让我们从 list 案例开始,因为它更容易重构:

retlist = []
for value in obj:
    child = dict_key_filter0(value, obj_filter)
    if child:
        retlist.append(child)

我们可以将其转化为简单的列表理解:

retlist = [dict_key_filter(value, obj_filter) for value in obj if dict_key_filter(value, obj_filter)]

缺点是dict_key_filter被计算了两次。我们可以通过一个小技巧来避免这种情况(参见):

retlist = [subtree for subtree in (dict_key_filter(value, obj_filter) for value in obj) if subtree]

内部表达式 (dict_key_filter(value, obj_filter) for value in obj) 是一个生成器,每个值调用一次 dict_key_filter。但是如果我们构建一个 dict_key_filter:

的闭包,我们甚至可以做得更好
def dict_key_filter(obj, obj_filter):
    def inner_dict_key_filter(obj): return dict_key_filter(obj, obj_filter)

    ...

    retlist = list(filter(len, map(inner_dict_key_filter, obj)))

现在我们处于函数世界:mapinner_dict_key_filter 应用于列表的每个元素,然后过滤子树以排除空子树(len(subtree) 为真当且仅当 subtree 不为空)。现在,代码如下所示:

def dict_key_filter(obj, obj_filter):
    def inner_dict_key_filter(obj): return dict_key_filter(obj, obj_filter)

    if isinstance(obj, dict):
        retdict = {}
        ...
        return retdict
    elif isinstance(obj, list):
        return list(filter(len, map(inner_dict_key_filter, obj)))
    else:
        return []

如果您熟悉函数式编程,list 案例是可读的(不像 Haskell 那样可读,但仍然可读)。

dict案例

我没有忘记您问题中的 dictionary-comprehension 标签。第一个想法是创建一个函数 return 分支的完整副本或 DFS 其余部分的结果。

def build_subtree(key, value):
    if key in obj_filter:
        return copy.deepcopy(value) # keep the branch
    elif isinstance(value, (dict, list)):
        return inner_dict_key_filter(value) # continue to search
    return [] # just an orphan value here

list的情况一样,我们暂时不拒绝空的subtree

retdict = {}
for key, value in obj.items():
    retdict[key] = build_subtree(key, value)

我们现在有了听写理解的完美案例:

retdict = {key: build_subtree(key, value) for key, value in obj.items() if build_subtree(key, value)}

同样,我们使用小技巧来避免计算两次值:

retdict = {key:subtree for key, subtree in ((key, build_subtree(key, value)) for key, value in obj.items()) if subtree}

但是这里有一个小问题:上面的代码并不完全等同于原始代码。如果值为 0 怎么办?在原始版本中,我们有 retdict[key] = copy.deepcopy(0) 但在新版本中我们什么都没有。 0 值被评估为 false 并被过滤。然后 dict 可能会变空,我们错误地切断了分支。我们需要另一个测试来确定我们要删除一个值:如果它是一个空列表或字典,则将其删除,否则保留它:

def to_keep(subtree): return not (isinstance(subtree, (dict, list)) or len(subtree) == 0)

即:

 def to_keep(subtree): return not isinstance(subtree, (dict, list)) or subtree

如果您还记得一点逻辑 (https://en.wikipedia.org/wiki/Truth_table#Logical_implication),您可以将其解释为:如果 subtree 是字典或列表,则它不能为空。

让我们把各个部分放在一起:

def dict_key_filter(obj, obj_filter):
    def inner_dict_key_filter(obj): return dict_key_filter(obj, obj_filter)
    def to_keep(subtree): return not isinstance(subtree, (dict, list)) or subtree

    def build_subtree(key, value):
        if key in obj_filter:
            return copy.deepcopy(value) # keep the branch
        elif isinstance(value, (dict, list)):
            return inner_dict_key_filter(value) # continue to search
        return [] # just an orphan value here

    if isinstance(obj, dict):
        key_subtree_pairs = ((key, build_subtree(key, value)) for key, value in obj.items())
        return {key:subtree for key, subtree in key_subtree_pairs if to_keep(subtree)}
    elif isinstance(obj, list):
        return list(filter(to_keep, map(inner_dict_key_filter, obj)))
    return []

我不知道这是否更像 pythonic,但对我来说似乎更清楚。

dict1 = {
    'test1': { 'test2':[1,2] }, 
    'test3': [
        {'test6': 2}, 
        {
            'test8': { 'test9': 23 }
        }
    ],
    'test4':{'test5': 0}
}

obj_filter = ['test5' , 'test9']

print (dict_key_filter(dict1, obj_filter))
# {'test3': [{'test8': {'test9': 23}}], 'test4': {'test5': 0}}