关于python:从嵌套字典中删除字段的优雅方法

Elegant way to remove fields from nested dictionaries

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

1
2
3
4
5
6
7
8
9
10
11
12
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

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


1
2
3
4
5
6
7
8
9
10
11
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
if type(dict_foo[field]) == dict:

这绝对可以改进。一般情况下(另见PEP8),您应该使用isinstance,而不是比较类型:

1
if isinstance(dict_foo[field], dict)

但是,如果dict_foo[field]dict的一个子类,那么这也将返回True。如果你不想这样做,你也可以用is代替==。这会稍微快一点(而且可能不明显)。

如果你还想允许任意听写的对象,你可以更进一步,测试它是不是一个collections.abc.MutableMapping。这将是dictdict子类的True,以及在不子类化dict的情况下显式实现该接口的所有可变映射,例如UserDict

1
2
3
4
5
6
7
8
>>> 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,但是ymmv。

从字典中删除键

您复制了字典以避免在迭代过程中删除键值对时出现问题。但是,正如另一个答案已经提到的,您可以迭代应该删除的键,并尝试删除它们:

1
2
3
4
5
for key in keys_to_remove:
    try:
        del dict[key]
    except KeyError:
        pass

这还有一个额外的优点,即您不需要嵌套两个循环(这可能会比较慢,尤其是当需要删除的键的数量非常长时)。

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

1
2
3
4
5
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听起来像是删除的dict。我倾向于使用dictionarydct这样的名称,因为函数名已经描述了对字典所做的操作。

  • lst_keys,那里也一样。我可能只在那里使用keys。如果你想更具体一些,比如keys_sequence会更有意义,因为它接受任何sequence(你只需要多次迭代),而不仅仅是列表。

  • dict_foo,只是不…

  • field也不太合适,它是一把钥匙。

把它们放在一起:

正如我之前所说,我会亲自修改字典的位置,不再返回字典。因此,我提出了两个解决方案,一个是在适当的地方修改它,但不返回任何内容,另一个是创建一个删除了键的新字典。

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

1
2
3
4
5
6
7
8
9
10
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)

以及返回新对象的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
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中(我在代码的适当位置放置了注释)。


由于这个问题要求一种优雅的方式,我将把我的通用解决方案提交给正在争论的嵌套结构。首先,用pip install boltons安装boltons实用程序包,然后:

1
2
3
4
5
6
7
8
9
10
11
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,所以它在任何地方都可以工作,并且在Python2.7和3.3+中进行了全面的测试。最重要的是,我是为这样的案件写的,所以如果你发现一个案件它不能处理,你可以让我纠正它就在这里。


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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)
    return d

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

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

此解决方案适用于给定嵌套的dict中的dictlist。输入to_delete可以是要删除的strlist或单个str

请注意,如果删除dict中的唯一键,将得到一个空的dict


因为您已经需要循环访问dict中的每个元素,所以我只使用一个循环,并确保使用一个集合来查找要删除的键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
2
3
4
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)) for key,value in dict_del.items()) if key not in lst_keys}


使用这篇文章中令人敬畏的代码并添加一个小语句:

1
2
3
4
5
6
    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}