优雅的方法从嵌套字典中删除字段

43

我需要从一个字典中删除一些字段,这些字段的键在一个列表中。因此我编写了以下函数:

def delete_keys_from_dict(dict_del, lst_keys):
    """
    Delete the keys present in lst_keys from the dictionary.
    Loops recursively over nested dictionaries.
    """
    dict_foo = dict_del.copy()  #Used as iterator to avoid the 'DictionaryHasChanged' error
    for field in dict_foo.keys():
        if field in lst_keys:
            del dict_del[field]
        if type(dict_foo[field]) == dict:
            delete_keys_from_dict(dict_del[field], lst_keys)
    return dict_del

这段代码可以工作,但它不太优雅,我相信有更好的解决方案。


4
我认为这不是糟糕的代码;你已经掌握了递归遍历嵌套字典的重要部分。你可能应该检查 isinstance(spam,collections.MutableMapping) 来使其更具多态性。 - Katriel
1
我不喜欢递归。如何在没有使用递归的情况下从嵌套字典中删除项目? - Golden Lion
我正在将我的R和Python知识进行桥接,我必须说,R和purrr包中的map函数使得处理列表/JSON等数据比我迄今在Python中找到的要容易得多。 - Brandon Rose MD MPH
11个回答

45

首先,我认为你的代码运行良好且不失优雅。没有直接的理由不使用你提供的代码。

然而,以下几点可以改进:

比较类型

你的代码包含以下行:

if type(dict_foo[field]) == dict:

这可以被明显地改进。通常情况下(请参见 PEP8),您应该使用 isinstance 而不是比较类型:

if isinstance(dict_foo[field], dict)

如果dict_foo[field]dict的子类,==运算符也会返回True。如果不想这样做,您可以改用is运算符。这将稍微快一点(可能不太明显)。

如果您还想允许任意的类dict对象,您可以进一步测试它是否是collections.abc.MutableMapping的实例。这将对于dictdict的子类以及所有显式实现了该接口而没有继承dict的可变映射(例如UserDict)都返回True

>>> from collections import MutableMapping
>>> # from UserDict import UserDict # Python 2.x
>>> from collections import UserDict  # Python 3.x - 3.6
>>> # from collections.abc import MutableMapping # Python 3.7+
>>> isinstance(UserDict(), MutableMapping)
True
>>> isinstance(UserDict(), dict)
False

原地修改和返回值

通常函数要么在原地修改数据结构,要么返回新的(经过修改的)数据结构。举几个例子: list.appenddict.cleardict.update 都会原地修改数据结构并返回None。这使得更容易跟踪函数的行为。但这不是一个硬性规则,总有例外情况。然而个人认为像这样的函数不需要成为这个例外, 我会简单地移除return dict_del 行,并让它隐式地返回None,但您的情况可能不同。

从字典中删除键

您复制了字典以避免迭代期间删除键值对时出现问题。然而,正如其他答案已经提到的,您可以直接迭代应该被删除的键并尝试删除它们:

for key in keys_to_remove:
    try:
        del dict[key]
    except KeyError:
        pass

这样做的额外优点是您不需要嵌套两个循环(可能会更慢,特别是当需要删除的键的数量非常多时)。

如果您不喜欢空的except语句,您也可以使用:contextlib.suppress (需要Python 3.4+):

from contextlib import suppress

for key in keys_to_remove:
    with suppress(KeyError):
        del dict[key] 

变量名称

有一些变量名我想要重命名,因为它们不是很具有描述性,甚至会误导:

  • delete_keys_from_dict 应该可能提到子字典处理,可以改成 delete_keys_from_dict_recursive

  • dict_del 听起来像是一个删除的字典。我倾向于使用类似 dictionarydct 这样的名称,因为函数名称已经描述了对字典所做的操作。

  • lst_keys,同样如此,我可能只会使用 keys。如果你想要更加具体的话,例如 keys_sequence 会更有意义,因为它接受任何 sequence(您只需要能够多次迭代它),而不仅仅是列表。

  • dict_foo 简直不行...

  • field 也不太合适,它是一个

综合考虑:

正如我之前所说,我个人会就地修改字典,而不是返回字典。因此,我提供了两种解决方案,一种是就地修改它而不返回任何内容,另一种则是创建一个新的字典并删除其中的键。

就地修改的版本(非常类似于Ned Batchelders的解决方案):

from collections import MutableMapping
from contextlib import suppress

def delete_keys_from_dict(dictionary, keys):
    for key in keys:
        with suppress(KeyError):
            del dictionary[key]
    for value in dictionary.values():
        if isinstance(value, MutableMapping):
            delete_keys_from_dict(value, keys)

同时返回一个新对象的解决方案:

from collections import MutableMapping

def delete_keys_from_dict(dictionary, keys):
    keys_set = set(keys)  # Just an optimization for the "if key in keys" lookup.

    modified_dict = {}
    for key, value in dictionary.items():
        if key not in keys_set:
            if isinstance(value, MutableMapping):
                modified_dict[key] = delete_keys_from_dict(value, keys_set)
            else:
                modified_dict[key] = value  # or copy.deepcopy(value) if a copy is desired for non-dicts.
    return modified_dict

然而它只复制字典,其他值不会作为副本返回,如果你希望这些值也被复制,你可以很容易地在这些值上使用copy.deepcopy进行深拷贝(我已在代码的适当位置放置了注释)。


4
哇!这是一个令人印象深刻的回答。我8年前问过这个问题,自那以后我学到了很多,但我不知道contextlib.suppressMutableMapping。非常感谢@MSeifert! - fasouto
1
这确实令人印象深刻。从中学到了很多。但是它缺少对列表的递归处理 :-(@Michael Dorner 在下面的回答中,有提到这一点。 - JdeHaan
@Mahmoud Hashemi 的下面的回答是最现代化的,除了没有原地执行它的任务。 - JdeHaan
我是Python的新手(有4年的R编程经验),但我有一个嵌套的JSON文件,一开始是一个字典,当你深入到里面时就会变成列表。所以当我尝试使用你的函数时,它可能无法通过列表元素。我正在尝试删除JSON文件的一些节点,但卡住了。在R中,我只需要执行LIST["node1"]["node2"]["node3"] <- NULL即可。 - Brandon Rose MD MPH
1
@BrandonRose 我认为一个包含有关您的数据及其结构的详细信息的新问题会更好。在那里,您还有更多选项来格式化文本和代码。 - MSeifert
1
对于Python3.10及以上版本,请使用from collections.abc import MutableMapping - Chris P

21
def delete_keys_from_dict(dict_del, lst_keys):
    for k in lst_keys:
        try:
            del dict_del[k]
        except KeyError:
            pass
    for v in dict_del.values():
        if isinstance(v, dict):
            delete_keys_from_dict(v, lst_keys)

    return dict_del

1
抱歉,但是这段代码并没有按照预期工作。我尝试执行以下操作:print delete_keys_from_dict ({'code': 'sdasda', 'tag.dbmko8e8': {'id':'casas', 'name': 'asdas identyfier'}, 'name': 'collection'}, ["id"])并且希望从字典中删除所有字段 :( - fasouto
1
我之前没有返回字典(我已经在上面更新了代码)。你打印出“None”的原因是因为值没有被返回。由于这个函数不会修改字典,所以你可以直接打印传入的同一个字典。我已经更新了代码,使其也返回了字典。 - Ned Batchelder
1
说实话,我认为你的第一个版本更好,不返回字典,因为正如你所说,原始字典已经具有更新后的键,你不会“浪费”返回值来返回已存在的内容,而且该方法将来可以修改,例如返回删除的值的数量,而不会对已存在的调用代码进行更改。 - laurent
糟糕!!我没有意识到缺少返回:(感谢你的代码,Ned! - fasouto
@fsouto - 我会使用修改为打印(或其他)语句的实际字典,而不是返回值,因为当您使用print delete_keys...时,您最终会有一个打印语句修改数据。当然,这是可以的,但个人上避免这样做,因为如果您不再需要打印字典,则不能仅删除打印语句,因为字典将不会更新,所以您必须记住并为其他程序员或未来数周的自己进行注释,我认为这是一个不必要的潜在错误来源 :) - laurent

17

由于问题要求一种优雅的方式,我将提交通用解决方案来处理嵌套结构。首先,使用pip install boltons安装boltons实用程序包,然后:

from boltons.iterutils import remap

data = {'one': 'remains', 'this': 'goes', 'of': 'course'}
bad_keys = set(['this', 'is', 'a', 'list', 'of', 'keys'])

drop_keys = lambda path, key, value: key not in bad_keys
clean = remap(data, visit=drop_keys)
print(clean)

# Output:
{'one': 'remains'}

简而言之,Remap 工具是处理现实世界数据结构的全功能但简洁方法,这些结构通常是嵌套的,甚至可能包含循环和特殊容器。

此页面包含更多示例,包括使用 Github 的 API 处理更大对象的示例。

它是纯 Python 的,因此可以在任何地方使用,并已在 Python 2.7 和 3.3+ 中进行了完全测试。最重要的是,我编写它就是为了处理像这样的情况,如果您找到它无法处理的情况,您可以在此处向我反馈。


2
请将以下与编程有关的内容从英语翻译成中文。“返回已翻译的文本”:这应该是正确答案,因为OP要求“优雅”的方式 - sal

12
def delete_keys_from_dict(d, to_delete):
    if isinstance(to_delete, str):
        to_delete = [to_delete]
    if isinstance(d, dict):
        for single_to_delete in set(to_delete):
            if single_to_delete in d:
                del d[single_to_delete]
        for k, v in d.items():
            delete_keys_from_dict(v, to_delete)
    elif isinstance(d, list):
        for i in d:
            delete_keys_from_dict(i, to_delete)

d = {'a': 10, 'b': [{'c': 10, 'd': 10, 'a': 10}, {'a': 10}], 'c': 1 }
delete_keys_from_dict(d, ['a', 'c']) # inplace deletion 
print(d)

>>> {'b': [{'d': 10}, {}]}

这个解决方案适用于给定嵌套字典中的dictlist。输入的to_delete可以是要删除的strlist,也可以是单个str
请注意,如果您删除了dict中唯一的键,您将得到一个空的dict

2
这个代码实现了我需要的功能,而首选答案则没有考虑递归到列表中。问题是:为什么要使用return?它可以直接在原地操作。 - JdeHaan
1
非常好的问题!为了保持一致性并明确这是一个就地方法,我应该删除返回语句!感谢您的提醒! - Michael Dorner

3
我认为以下方法更加优雅:
def delete_keys_from_dict(dict_del, lst_keys):
    if not isinstance(dict_del, dict):
        return dict_del
    return {
        key: value
        for key, value in (
            (key, delete_keys_from_dict(value, lst_keys))
            for key, value in dict_del.items()
        )
        if key not in lst_keys
    }

使用示例:

test_dict_in = {
    1: {1: {0: 2, 3: 4}},
    0: {2: 3},
    2: {5: {0: 4}, 6: {7: 8}},
}

test_dict_out = {
    1: {1: {3: 4}},
    2: {5: {}, 6: {7: 8}},
}

assert delete_keys_from_dict(test_dict_in, [0]) == test_dict_out

我同意,这也是我的方法,不可变性。 - user1556435

2

既然您已经需要遍历字典中的每个元素,我建议您仅使用一个循环,并确保使用集合来查找要删除的键。

def delete_keys_from_dict(dict_del, the_keys):
    """
    Delete the keys present in the lst_keys from the dictionary.
    Loops recursively over nested dictionaries.
    """
    # make sure the_keys is a set to get O(1) lookups
    if type(the_keys) is not set:
        the_keys = set(the_keys)
    for k,v in dict_del.items():
        if k in the_keys:
            del dict_del[k]
        if isinstance(v, dict):
            delete_keys_from_dict(v, the_keys)
    return dict_del

1
@Ned Batchelder:我们有办法可以反转这个过程吗?我的意思是只保留特定的键,删除不在列表中的其他键。 - Ujjawal Khare

1
这适用于包含Iterablelist等)的dict,其中可能包含dict。适用于Python 3。对于Python 2,unicode也应该从迭代中排除。还可能有一些不起作用的可迭代对象,我不知道。(即会导致无限递归)
from collections.abc import Iterable

def deep_omit(d, keys):
    if isinstance(d, dict):
        for k in keys:
            d.pop(k, None)
        for v in d.values():
            deep_omit(v, keys)
    elif isinstance(d, Iterable) and not isinstance(d, str):
        for e in d:
            deep_omit(e, keys)

    return d

1

由于没有人发布一个交互版本,这对某些人可能很有用:

def delete_key_from_dict(adict, key):
    stack = [adict]
    while stack:
        elem = stack.pop()
        if isinstance(elem, dict):
            if key in elem:
                del elem[key]
            for k in elem:
                stack.append(elem[k])

这个版本可能是你要推送到生产环境的版本。递归版本虽然优雅且易于编写,但它的可扩展性不佳(默认情况下,Python使用最大递归深度为1000)。


有人可以帮忙确定这个算法的上限吗?我猜是O(n^2)。 - funnydman
这实际上是一种树遍历,其中每个节点只被访问一次。树遍历的时间复杂度为O(N) - ingyhere
如果还有嵌套字典的列表、元组或集合,你可以添加额外的分析来涵盖所有情况:if isinstance(elem, (list, tuple, set)): for k in elem: stack.append(k) elif isinstance()... - ingyhere

0

如果您还有嵌套键,并且基于@John La Rooy的答案,这是一种优雅的解决方案:

from boltons.iterutils import remap


def sof_solution():
    data = {"user": {"name": "test", "pwd": "******"}, "accounts": ["1", "2"]}
    sensitive = {"user.pwd", "accounts"}

    clean = remap(
        data,
        visit=lambda path, key, value: drop_keys(path, key, value, sensitive)
    )
    print(clean)


def drop_keys(path, key, value, sensitive):
    if len(path) > 0:
        nested_key = f"{'.'.join(path)}.{key}"
        return nested_key not in sensitive
    return key not in sensitive

sof_solution() # prints {'user': {'name': 'test'}}

-1

使用这篇文章中的精彩代码,并添加一个小语句:

    def remove_fields(self, d, list_of_keys_to_remove):
        if not isinstance(d, (dict, list)):
            return d
        if isinstance(d, list):
            return [v for v in (self.remove_fields(v, list_of_keys_to_remove) for v in d) if v]
        return {k: v for k, v in ((k, self.remove_fields(v, list_of_keys_to_remove)) for k, v in d.items()) if k not in list_of_keys_to_remove}

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