优雅地从嵌套字典中移除字段

43 投票
11 回答
70482 浏览
提问于 2025-04-16 02:18

我需要从一个字典中删除一些字段,而这些字段的键在一个列表里。所以我写了这个函数:

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

这个代码可以运行,但看起来不太优雅,我相信一定有更好的解决办法。

11 个回答

18

因为这个问题想要一个优雅的解决方案,所以我来分享一个通用的方法来处理嵌套结构。首先,你需要安装一个叫做 boltons 的工具包,可以通过命令 pip install 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以上的版本中经过了全面测试。最棒的是,我就是为了像这样的情况写的这个工具,所以如果你发现它不能处理某种情况,可以在 这里找我反馈,我会帮你修复的。

22

当然可以!请看下面的内容:

在编程中,有时候我们需要让程序在特定的条件下执行某些操作。这就像给程序设定了一些规则,只有当这些规则被满足时,程序才会做出反应。

比如说,你可能希望在用户点击一个按钮的时候,程序才会显示一条消息。这个过程就叫做“事件处理”。简单来说,就是程序在等待某个事件发生,然后根据这个事件来决定接下来要做什么。

在代码中,我们通常会使用一些特定的语法来设置这些事件和相应的操作。这样,当事件发生时,程序就会自动执行我们预先设定好的指令。

希望这个解释能帮助你更好地理解事件处理的概念!

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
45

首先,我觉得你的代码是可以工作的,并不是不优雅。没有什么理由不使用你提供的代码。

不过,有一些地方可以改进:

比较类型

你的代码中有一行:

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

这部分是可以改进的。一般来说(具体可以参考PEP8),你应该使用isinstance来判断类型,而不是直接比较类型:

if isinstance(dict_foo[field], dict)

不过,如果dict_foo[field]dict的子类,这样的判断也会返回True。如果你不想要这种情况,可以用is来代替==。这样会稍微快一点(可能你不会注意到)。

如果你还想允许任意类似字典的对象,可以进一步测试它是否是collections.abc.MutableMapping。这样对于dict及其子类,以及所有明确实现了该接口的可变映射(而不是继承自dict)都会返回True,比如UserDict

>>> 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都是就地修改数据结构,并且return 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 Batchelder的解决方案):

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来包装这些(我在代码的适当位置加了注释)。

撰写回答