Python - 递归删除字典键?

10
我正在使用Python 2.7和plistlib来以嵌套的字典/数组形式导入.plist文件,然后查找特定的键并删除它。对于我们在办公室使用的实际文件,我已经知道如何找到这些值--但我编写脚本的想法是如果文件结构发生变化或我们需要对其他类似的文件进行同样的操作,我就不必在将来进行更改。不幸的是,我似乎正在尝试在迭代过程中修改字典,但我不确定实际上发生了什么,因为我正在使用iteritems()enumerate()来获取生成器并与其一起工作,而不是直接使用对象。
def scrub(someobject, badvalue='_default'): ##_default isn't the real variable
    """Walks the structure of a plistlib-created dict and finds all the badvalues and viciously eliminates them.

Can optionally be passed a different key to search for."""
    count = 0

    try:
        iterator = someobject.iteritems()
    except AttributeError:
        iterator = enumerate(someobject)

    for key, value in iterator:
        try:
            scrub(value)
        except:
            pass
        if key == badvalue:
            del someobject[key]
            count += 1

    return "Removed {count} instances of {badvalue} from {file}.".format(count=count, badvalue=badvalue, file=file)

不幸的是,当我在我的测试.plist文件上运行时,会出现以下错误:

Traceback (most recent call last):
  File "formscrub.py", line 45, in <module>
    scrub(loadedplist)
  File "formscrub.py", line 19, in scrub
    for key, value in iterator:
RuntimeError: dictionary changed size during iteration

因此,问题可能是对自身的递归调用,但即使如此,它也只应从原始对象中删除。我不确定如何避免递归(或者这是否是正确的策略),但由于它是一个.plist文件,我确实需要能够识别何时出现字典或列表,并在搜索中迭代它们以寻找(a)更多要搜索的字典,或(b)需要删除的导入.plist中的实际键值对。
最终,这是一个部分性的非问题,因为我将定期使用的文件具有已知的结构。然而,我真的希望创建一些东西,不关心正在处理的对象的嵌套或顺序,只要它是一个带有数组的Python字典即可。

2
这个问题具体是什么? - jdotjdot
啊,天哪 :/ 我甚至还没有涉及到相关的难题。 - user890167
4个回答

17

在迭代序列时添加或删除其中的项可能会很棘手,而对于字典来说则是非法的(正如你刚刚发现的)。在迭代字典时删除条目的正确方法是迭代键的快照。在Python 2.x中,dict.keys()提供了这样的快照。因此,对于字典的解决方案是:

for key in mydict.keys():
    if key == bad_value:
        del mydict[key]

正如cpizza在评论中提到的那样,对于Python 3,您需要使用list() 显式创建快照:

for key in list(mydict.keys()):
    if key == bad_value:
        del mydict[key]

对于列表而言,尝试在索引的快照上进行迭代(即for i in len(thelist):)会导致IndexError,因为一旦删除任何内容,至少最后一个索引将不存在,即使没有删除任何内容,索引序列也可能与列表本身不同步,您可能会跳过一个或多个项目。 enumerate可以安全地防止IndexError(因为当列表中没有更多“下一个”项时,迭代会自动停止),但仍然会跳过项目:

>>> mylist = list("aabbccddeeffgghhii")
>>> for x, v  in enumerate(mylist):
...     if v in "bdfh":
...         del mylist[x]
>>> print mylist
['a', 'a', 'b', 'c', 'c', 'd', 'e', 'e', 'f', 'g', 'g', 'h', 'i', 'i']

你可以看到,这并不是一个很成功的例子。

这里已知的解决方案是迭代反向索引,即:

>>> mylist = list("aabbccddeeffgghhii")
>>> for x in reversed(range(len(mylist))):
...     if mylist[x] in "bdfh":
...         del mylist[x]
>>> print mylist
['a', 'a', 'c', 'c', 'e', 'e', 'g', 'g', 'i', 'i']

这也适用于反向枚举,但我们并不关心。
因此,总结一下:您需要为字典和列表准备两种不同的代码路径,并且还需要注意“非容器”值(既不是列表也不是字典的值),这是您当前代码中未处理的部分。
def scrub(obj, bad_key="_this_is_bad"):
    if isinstance(obj, dict):
        # the call to `list` is useless for py2 but makes
        # the code py2/py3 compatible
        for key in list(obj.keys()):
            if key == bad_key:
                del obj[key]
            else:
                scrub(obj[key], bad_key)
    elif isinstance(obj, list):
        for i in reversed(range(len(obj))):
            if obj[i] == bad_key:
                del obj[i]
            else:
                scrub(obj[i], bad_key)

    else:
        # neither a dict nor a list, do nothing
        pass

作为附注:绝对不要编写裸的except子句。绝对永远不要这样做。实际上,这应该是非法语法。

1
@Stick: 你对迭代器的理解确实不完全。你可以在这里找到官方文档:http://docs.python.org/2/library/stdtypes.html#iterator-types。关于 TypeError,你张贴的代码显然有同样的问题 - 尝试 enumerate(42) - bruno desthuilliers
1
@Stick: 修改后的答案。 - bruno desthuilliers
1
有太多可迭代类型(以及一些具有“dict”API子部分的非内置非字典类型)可以期望完全通用但简单易维护的代码,因此最好坚持KISS和YAGNI。这个解决方案应该适用于任何字典和列表的组合,因此您已经掌握了基础知识。如果您最终需要处理更多类型,我建议使用类似Py3 GenericFunctions的东西(http://www.python.org/dev/peps/pep-0443/)-至少有一个2.x的实现(参见http://www.python.org/dev/peps/pep-3119/#abcs-vs-generic-functions)。 - bruno desthuilliers
1
哦,没错:类型检查本身并没有问题——就像任何其他“黄金法则”一样,它主要是理解规则适用的时候和不适用的时候。我们在这里做的不是基于类型的限制,而是(最原始的形式)基于类型的分派。 - bruno desthuilliers
1
对于Python 3,scrub方法的第三行应该写成for k in list(obj.keys())。如果不创建列表副本就直接迭代修改后的字典会导致程序崩溃。 - ccpizza
显示剩余3条评论

7
这是@bruno desthuilliers的一个通用版本,其中包含一个可调用对象来对键进行测试。
def clean_dict(obj, func):
    """
    This method scrolls the entire 'obj' to delete every key for which the 'callable' returns
    True

    :param obj: a dictionary or a list of dictionaries to clean
    :param func: a callable that takes a key in argument and return True for each key to delete
    """
    if isinstance(obj, dict):
        # the call to `list` is useless for py2 but makes
        # the code py2/py3 compatible
        for key in list(obj.keys()):
            if func(key):
                del obj[key]
            else:
                clean_dict(obj[key], func)
    elif isinstance(obj, list):
        for i in reversed(range(len(obj))):
            if func(obj[i]):
                del obj[i]
            else:
                clean_dict(obj[i], func)

    else:
        # neither a dict nor a list, do nothing
        pass

这里有一个使用可调用的正则表达式的示例:

func = lambda key: re.match(r"^<div>", key)

clean_dict(obj, func)

1
这是一个用于递归删除任意对象中键的代码。
def remove_keys_recursively(dict_obj, keys):
    for key in list(dict_obj.keys()):
        if not isinstance(dict_obj, dict):
            continue
        elif key in keys:
            dict_obj.pop(key, None)
        elif isinstance(dict_obj[key], dict):
            remove_keys_recursively(dict_obj[key], keys)
        elif isinstance(dict_obj[key], list):
            for item in dict_obj[key]:
                remove_keys_recursively(item, keys)
    return

输出:

>>> d = {1:{2:3}, 2:{3:4}, 5:{6:{2:3}, 7:{1:2, 2:3}}, 3:4}
>>>
>>> d
{1: {2: 3}, 2: {3: 4}, 5: {6: {2: 3}, 7: {1: 2, 2: 3}}, 3: 4}
>>>
>>> keys = [2]
>>>
>>> remove_keys_recursively(d, keys)
>>>
>>> d
{1: {}, 5: {6: {}, 7: {1: 2}}, 3: 4}

易于阅读和理解的解决方案。谢谢! - Hamodey_
if not isinstance(dict_obj, dict): 这个检查似乎放错了位置?将其放在循环内部没有意义,因为如果它不是一个字典,那么.keys()在上一行可能已经失败了。 - undefined

0
def walk(d, badvalue, answer=None, sofar=None):
    if sofar is None:
        sofar = []
    if answer is None:
        answer = []
    for k,v in d.iteritems():
        if k == badvalue:
            answer.append(sofar + [k])
        if isinstance(v, dict):
            walk(v, badvalue, answer, sofar+[k])
    return answer

def delKeys(d, badvalue):
    for path in walk(d, badvalue):
        dd = d
        while len(path) > 1:
            dd = dd[path[0]]
            path.pop(0)
        dd.pop(path[0])

输出

In [30]: d = {1:{2:3}, 2:{3:4}, 5:{6:{2:3}, 7:{1:2, 2:3}}, 3:4}

In [31]: delKeys(d, 2)

In [32]: d
Out[32]: {1: {}, 3: 4, 5: {6: {}, 7: {1: 2}}}

如果 .plists 只是嵌套字典,那就太棒了,但不幸的是,我正在处理的这些文件还包含数组。但我非常喜欢这个想法,我认为这是朝着正确方向迈出的一步。 - user890167
如果您能够发布一下您正在处理的数据结构示例,我可以尝试更新我的答案。 - inspectorG4dget

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