如何移除嵌套字典中的所有空字段?

16

如果我有一个字典,其中某些字段的值可能是一个字典或者一个数组。如何删除其中所有的空字段?

这里的“空字段”表示一个字段的值为空数组([])、None,或者空字典(所有子字段都为空)。

例如: 输入:

{
    "fruit": [
        {"apple": 1},
        {"banana": None}
    ],
    "veg": [],
    "result": {
        "apple": 1,
        "banana": None
    }
}

输出:

{
    "fruit": [
        {"apple": 1}
    ],
    "result": {
        "apple": 1
    }
}
5个回答

39

使用递归函数返回一个新字典:

def clean_empty(d):
    if isinstance(d, dict):
        return {
            k: v 
            for k, v in ((k, clean_empty(v)) for k, v in d.items())
            if v
        }
    if isinstance(d, list):
        return [v for v in map(clean_empty, d) if v]
    return d
{..}构造是一个字典推导式;它仅包含原始字典中v为真的键,例如不为空。类似地,[..]构造生成一个列表。

嵌套的(.. for ..)构造是一种生成器表达式,允许代码在递归之后紧凑地过滤空对象。

构建这样一个函数的另一种方法是使用@singledispatch装饰器;然后您编写多个函数,每个函数针对一个对象类型:

from functools import singledispatch

@singledispatch
def clean_empty(obj):
    return obj

@clean_empty.register
def _dicts(d: dict):
    items = ((k, clean_empty(v)) for k, v in d.items())
    return {k: v for k, v in items if v}

@clean_empty.register
def _lists(l: list):
    items = map(clean_empty, l)
    return [v for v in items if v]

上述的@singledispatch版本与第一个函数完全相同,但现在基于注册函数的类型注释,由装饰器实现处理isinstance()测试。我还将嵌套迭代器(生成器表达式和map()函数)放入单独的变量中,以进一步提高可读性。

请注意,任何设置为数字0(整数0、浮点数0.0)的值也将被清除。您可以使用if v or v == 0保留数字0值。

第一个函数的演示:

>>> sample = {
...     "fruit": [
...         {"apple": 1},
...         {"banana": None}
...     ],
...     "veg": [],
...     "result": {
...         "apple": 1,
...         "banana": None
...     }
... }
>>> def clean_empty(d):
...     if isinstance(d, dict):
...         return {
...             k: v
...             for k, v in ((k, clean_empty(v)) for k, v in d.items())
...             if v
...         }
...     if isinstance(d, list):
...         return [v for v in map(clean_empty, d) if v]
...     return d
... 
>>> clean_empty(sample)
{'fruit': [{'apple': 1}], 'result': {'apple': 1}}

8
如果你想处理常常嵌套、甚至包含循环和其他容器的真实数据结构,而且希望有一个全面而简洁的方法,我建议看看Boltons实用程序包中的remap实用程序
在执行pip install boltons或将iterutils.py复制到你的项目后,只需执行以下操作:
from boltons.iterutils import remap

data = {'veg': [], 'fruit': [{'apple': 1}, {'banana': None}], 'result': {'apple': 1, 'banana': None}}

drop_falsey = lambda path, key, value: bool(value)
clean = remap(data, visit=drop_falsey)
print(clean)

# Output:
{'fruit': [{'apple': 1}], 'result': {'apple': 1}}

此页面有许多更多的示例,包括使用Github API中更大的对象。

它是纯Python编写的,因此可以在任何地方运行,并且在Python 2.7和3.3+中进行了全面测试。最重要的是,我正是为这种情况编写它的,因此如果您发现它无法处理某种情况,可以在这里向我反馈以修复它。


0
def not_empty(o):
    # you can define what is empty.
    if not (isinstance(o, dict) or isinstance(o, list)):
        return True
    return len(o) > 0


def remove_empty(o):
    # here to choose what container you not need to recursive or to remove
    if not (isinstance(o, dict) or isinstance(o, list)):
        return o
    if isinstance(o, dict):
        return {k: remove_empty(v) for k, v in o.items() if not_empty(v)}
    if isinstance(o, list):
        return [remove_empty(v) for v in o if not_empty(v)]

“仅有代码的答案并不是高质量的答案”。虽然这段代码可能很有用,但您可以通过解释它为什么有效、如何有效、何时应该使用以及其限制是什么来改进它。请编辑您的答案,包括解释和相关文档链接。 - Muhammad Mohsin Khan

0

@mojoken - 用这个来克服布尔问题怎么样?

def clean_empty(d):
if not isinstance(d, (dict, list)):
    return d
if isinstance(d, list):
    return [v for v in (clean_empty(v) for v in d) if isinstance(v, bool) or v]
return {k: v for k, v in ((k, clean_empty(v)) for k, v in d.items()) if isinstance(v, bool) or v}

-2
def remove_empty_fields(data_):
    """
        Recursively remove all empty fields from a nested
        dict structure. Note, a non-empty field could turn
        into an empty one after its children deleted.

        :param data_: A dict or list.
        :return: Data after cleaning.
    """
    if isinstance(data_, dict):
        for key, value in data_.items():

            # Dive into a deeper level.
            if isinstance(value, dict) or isinstance(value, list):
                value = remove_empty_fields(value)

            # Delete the field if it's empty.
            if value in ["", None, [], {}]:
                del data_[key]

    elif isinstance(data_, list):
        for index in reversed(range(len(data_))):
            value = data_[index]

            # Dive into a deeper level.
            if isinstance(value, dict) or isinstance(value, list):
                value = remove_empty_fields(value)

            # Delete the field if it's empty.
            if value in ["", None, [], {}]:
                data_.pop(index)

    return data_

1
不行,伙计。它遇到了“RuntimeError: dictionary changed size during iteration”错误。 - waqasgard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接