【问题标题】:Elegant way to remove fields from nested dictionaries从嵌套字典中删除字段的优雅方法
【发布时间】:2010-08-04 12:58:47
【问题描述】:

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

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

这段代码有效,但不是很优雅,我相信有更好的解决方案。

【问题讨论】:

  • 我觉得代码不错;你已经得到了在嵌套字典上递归的重要部分。您可能应该检查isinstance( spam, collections.MutableMapping ) 是否更具多态性。
  • 我不喜欢递归。如何在不递归的情况下从嵌套字典中删除项目?

标签: python dictionary


【解决方案1】:

首先,我认为您的代码正在运行,而不是不优雅。没有直接的理由不使用您提供的代码。

但有几件事可能会更好:

比较类型

您的代码包含以下行:

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 的情况下显式实现该接口的可变映射,这将是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,但YMMV。

从字典中删除键

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

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 可能应该提到 subdict-handling,可能是 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 中(我在代码的适当位置添加了注释)。

【讨论】:

  • 哇!这是一个令人印象深刻的答案。我 8 年前问过这个问题,从那以后我学到了很多东西,但是我不知道 contextlib.suppressMutableMapping。非常感谢@MSeifert!
  • 这确实令人印象深刻。从中吸取了教训。它错过了递归到列表中,虽然:-(@Michael Dorner 下面的回答没有。
  • @Mahmoud Hashemi 下面的答案是最现代的,除了没有“就地”做这件事。
【解决方案2】:
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

【讨论】:

  • 抱歉,这段代码不能按预期工作,我尝试这样做: print delete_keys_from_dict ({'code': 'sdasda', 'tag.dbmko8e8': {'id':'casas', 'name': 'asdas identyfier'}, 'name': 'collection'}, ["id"]) 并从字典中删除所有字段:(
  • 我没有返回字典(我已经更新了上面的代码)。您正在打印“无”,因为没有返回该值。由于这个函数不会修改字典,你可以简单地打印你传入的同一个字典。我已经更新了代码,所以它也返回了字典。
  • tbh 我认为您的第一个版本更好,不返回字典,因为正如您所说,原始版本已经具有更新的键,并且您不会“浪费”返回值来返回已经存在的内容,并且将来可以修改方法以返回例如在不更改已经存在的调用代码的情况下删除的值的数量。
  • 糟糕!我没有意识到缺少回报 :( 感谢您的代码 Ned!
  • @fsouto - 我会使用为 print(或其他)语句修改的实际字典,而不是使用 print delete_keys... 时的返回值,您最终会使用 print 语句修改数据。当然这没关系,但我个人避免这样做,因为如果你不再需要打印字典,你不能只删除打印语句,因为字典不会更新,所以你必须记住并在几周内为其他程序员或您评论它,我认为这是不必要的潜在错误来源:)
【解决方案3】:

由于问题要求采用一种优雅的方式,我将提交我的通用解决方案来解决嵌套结构。首先,安装boltons utility packagepip 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'}

简而言之,the remap utility 是一种功能齐全但简洁的方法,可以处理经常嵌套的实际数据结构,甚至可以包含循环和特殊容器。

This page 有更多示例,包括处理来自 Github API 的更大对象的示例。

它是纯 Python,因此可以在任何地方使用,并且在 Python 2.7 和 3.3+ 中经过全面测试。最重要的是,我是为这样的情况写的,所以如果你发现它不能处理的情况,你可以让我修复它right here

【讨论】:

  • 这应该是正确的答案,因为 OP 要求一种“优雅”的方式
【解决方案4】:
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}, {}]}

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

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

【讨论】:

  • 这可以满足我的需要,首选答案不会,因为它错过了递归到列表中。问:为什么要退货?它就地工作。
  • 好问题!为了保持一致性并明确表明这是一种就地方法,我应该删除返回!谢谢你的评论!
【解决方案5】:

我认为下面的更优雅:

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}

【讨论】:

  • 我同意,这也是我的方法,不变性
【解决方案6】:

由于您已经需要遍历字典中的每个元素,所以我会坚持使用单个循环,并确保使用一个集合来查找要删除的键

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

【讨论】:

  • @Ned Batchelder:有没有办法可以扭转这种局面?我的意思是只保留特定的键并删除不在列表中的其余键?
【解决方案7】:

这适用于包含Iterables(list,...)的dicts,可能包含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

【讨论】:

    【解决方案8】:

    如果您也有 嵌套 键并且基于@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'}}
    

    【讨论】:

      【解决方案9】:

      使用来自this 帖子的精彩代码并添加一个小语句:

          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}
      

      【讨论】:

        【解决方案10】:

        我来这里是为了寻找从深度嵌套的 Python3 dicts 中删除键的解决方案,所有解决方案似乎都有些复杂。

        这是一个用于从嵌套或平面字典中删除键的单行器:

        nested_dict = {
            "foo": {
                "bar": {
                    "foobar": {},
                    "shmoobar": {}
                }
            }
        }
        
        >>> {'foo': {'bar': {'foobar': {}, 'shmoobar': {}}}}
        
        nested_dict.get("foo", {}).get("bar", {}).pop("shmoobar", None)
        
        >>> {'foo': {'bar': {'foobar': {}}}}
        

        我使用 .get() 来不获取 KeyError,并且我还提供了空字典作为默认值,直到链的末尾。我对最后一个元素执行 pop(),并在此处提供 None 作为默认值以避免 KeyError。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-04-14
          • 1970-01-01
          • 2017-03-08
          相关资源
          最近更新 更多