递归删除所有值为 NULL 的键

Recursive remove all keys that have NULL as value

我有一个复杂的 dict 数组,它有一些值作为字符串“NULL”,我想删除,我的字典看起来像这样:

d = [{
  "key1": "value1",
  "key2": {
    "key3": "value3",
    "key4": "NULL",
    "z": {
       "z1": "NULL",
       "z2": "zzz",
    },
  },
  "key5": "NULL"
}, {
  "KEY": "NULL",
  "AAA": "BBB",
}]

我想清除所有值为 NULL 的键

像这样:

[{"key1": "value1", "key2": {"key3": "value3", "z": {"z2": "zzz"}}}, {"AAA": "BBB"}]

我用的是Python3.9,所以可以用海象算子。

解法:

d = [{
  "key1": "value1",
  "key2": {
    "key3": "value3",
    "key4": "NULL",
    "z": {
       "z1": "NULL",
       "z2": "zzz",
    },
  },
  "key5": "NULL"
}, {
  "KEY": "NULL",
  "AAA": "BBB",
}]



for element in list(d):
    for key, value in element.copy().items():
        if value == "NULL":
            element.pop(key, None)
        elif isinstance(value, dict):
            for inner_key, inner_value in value.copy().items():
                if inner_value == "NULL":
                    value.pop(inner_key, None)
                elif isinstance(inner_value, dict):
                    for nested_inner_key, nested_inner_value in inner_value.copy().items():
                        if nested_inner_value == "NULL":
                            inner_value.pop(nested_inner_key, None)
print(d)

输出:

[{'key1': 'value1', 'key2': {'key3': 'value3', 'z': {'z2': 'zzz'}}}, {'AAA': 'BBB'}]

对每个字典/嵌套字典执行 .copy(),否则你会得到 this 错误。

同样检查 here

以下是使用递归执行此操作的方法:

def remove_null(d):
    if isinstance(d, list):
        for i in d:
            remove_null(i)
    elif isinstance(d, dict):
        for k, v in d.copy().items():
            if v == 'NULL':
                d.pop(k)
            else:
                remove_null(v)

d = [{
  "key1": "value1",
  "key2": {
    "key3": "value3",
    "key4": "NULL",
    "z": {
       "z1": "NULL",
       "z2": "zzz",
    },
  },
  "key5": "NULL"
}, {
  "KEY": "NULL",
  "AAA": "BBB",
}]

remove_null(d)
print(d)

输出:

[{"key1": "value1", "key2": {"key3": "value3", "z": {"z2": "zzz"}}}, {"AAA": "BBB"}]

由于您要求规范的答案,这里有一种方法可以避免就地编辑提供的对象,提供类型提示,具有文档字符串,可以使用 remove 参数调用以删除不同的值,并根据情况使用 TypeError 错误:

from collections import abc
from typing import Dict, List, Union

def without_nulls(d: Union[List, Dict], remove: tuple = ('NULL', 'null')) -> Union[List, Dict]:
    """ Given a list or dict, returns an object of the same structure without null dict values. """
    collection_types = (list, tuple) # avoid including 'str' here
    mapping_types = (abc.Mapping,)
    all_supported_types = (*mapping_types, *collection_types)
    if isinstance(d, collection_types):
        return [without_nulls(x, remove) if isinstance(x, all_supported_types) else x for x in d]
    elif isinstance(d, mapping_types):
        clean_val = lambda x: without_nulls(x, remove) if isinstance(x, all_supported_types) else x
        return {k: clean_val(v) for k, v in d.items() if v not in remove}
    raise TypeError(f"Unsupported type '{type(d)}': {d!r}")

用法:

>>> cleaned_d = without_nulls(d)
>>> cleaned_d
[{'key1': 'value1', 'key2': {'key3': 'value3', 'z': {'z2': 'zzz'}}}, {'AAA': 'BBB'}]
>>> d # d is unchanged
[{'key1': 'value1', 'key2': {'key3': 'value3', 'key4': 'NULL', 'z': {'z1': 'NULL', 'z2': 'zzz'}}, 'key5': 'NULL'}, {'KEY': 'NULL', 'AAA': 'BBB'}]

如果我想让它删除 None 而不是 'NULL',我可以这样调用它:

without_nulls(d, remove=(None,))

这是一个更通用的版本 - 我希望在通用 Python 工具包中找到的那种东西:

from collections import abc
from typing import Any, Callable, Dict, List, Optional, Union

def filter_dicts(d: Union[List, Dict],
                 value_filter: Optional[Callable[[Any], bool]] = None) -> Union[List, Dict]:
    """
    Given a list or dict, returns an object of the same structure without filtered values.

    By default, key-value pairs where the value is 'None' are removed. The `value_filter` param
    must be a function which takes values from the provided dict/list structure, and returns a
    truthy value if the key-value pair is to be removed, and a falsey value otherwise.
    """
    collection_types = (list, tuple) # avoid including 'str' here
    mapping_types = (abc.Mapping,)
    all_supported_types = (*mapping_types, *collection_types)
    if value_filter is None:
        value_filter = lambda x: x is None
    if isinstance(d, collection_types):
        return [filter_dicts(x, value_filter) if isinstance(x, all_supported_types) else x for x in d]
    elif isinstance(d, mapping_types):
        clean_val = lambda x: filter_dicts(x, value_filter) if isinstance(x, all_supported_types) else x
        return {k: clean_val(v) for k, v in d.items() if not value_filter(v)}
    raise TypeError(f"Unsupported type '{type(d)}': {d!r}")

我们可以这样使用:

>>> filter_dicts(d)
[{'key1': 'value1', 'key2': {'key3': 'value3', 'key4': 'NULL', 'z': {'z1': 'NULL', 'z2': 'zzz'}}, 'key5': 'NULL'}, {'KEY': 'NULL', 'AAA': 'BBB'}]
>>> filter_dicts(d) == d
True
>>> filter_dicts(d, lambda x: x in (None, 'NULL', 'null'))
[{'key1': 'value1', 'key2': {'key3': 'value3', 'z': {'z2': 'zzz'}}}, {'AAA': 'BBB'}]

这个版本的好处是它允许调用者完全自定义删除值时使用的标准。该函数处理遍历结构,并构建一个要返回的新结构,而调用者可以决定删除什么。

I am using Python 3.9, so it is possible to use walrus operator.

那么,让我们把一个扔进去,因为没有其他人这样做:

from collections import deque

def remove_null(thing):
    if isinstance(thing, list):
        deque(map(remove_null, thing), maxlen=0)
    elif isinstance(thing, dict):
        for key in thing.copy():
            if (value := thing[key]) == 'NULL':
                del thing[key]
            else:
                remove_null(value)

请注意,我使用 map 而不是循环来显示其他答案的替代方案。但是,这造成了我在@AnnZen 上评论过的相同情况,使用 returns 结果然后忽略它的操作。为了强制 map 惰性求值,我使用一个空的 deque 来消耗它。