python - 递归删除字典键?

时间:2013-12-19 22:13:19

标签: python recursion dictionary plist

我正在使用带有plistlib的Python 2.7以嵌套的dict /数组形式导入.plist,然后查找特定的键并将其删除,无论我在哪里看到它。

当谈到我们在办公室工作的实际文件时,我已经知道在哪里可以找到这些值了 - 但是我写了我的脚本,其中的想法是我没有,希望我不会如果文件结构发生变化,或者我们需要对其他类似文件进行更改,则必须在将来进行更改。

不幸的是,我似乎试图在迭代时修改dict,但我不确定它是如何实际发生的,因为我正在使用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)更多dicts to search,或(b)导入的.plist中我需要删除的实际键值对。

最终,这是一个部分非问题,因为我将定期处理的文件具有已知结构。但是,我真的希望创建一些不关心它正在使用的对象的嵌套或顺序的东西,只要它是一个包含数组的Python dict。

3 个答案:

答案 0 :(得分:5)

在迭代这个序列时向序列中添加项目或从序列中删除项目充其量是棘手的,而且只是非法(正如您刚才发现的)使用dicts。在迭代它时从dict中删除条目的正确方法是迭代键的快照。在Python 2.x中,dict.keys()提供了这样的快照。因此,对于dicts,解决方案是:

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

正如cpizza在评论中提到的,对于python3,你需要使用list()明确地创建快照:

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

对于列表,尝试迭代索引的快照(即for i in len(thelist):)会在删除任何内容后立即导致IndexError(显然,因为至少最后一个索引将不再存在),甚至如果不是,您可以跳过一个或多个项目(因为删除项目会使索引序列与列表本身不同步)。 enumerate对IndexError是安全的(因为当列表中没有'next'项时迭代会自动停止,但你仍然会跳过项目:

>>> 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']

这也适用于反向枚举,但我们并不在乎。

总结一下:你需要两个不同的代码和列表代码路径 - 你还需要处理“not container”值(既不是列表也不是dicts的值),你不需要处理的事情。目前的代码。

def scrub(obj, bad="_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(mydict.keys()):
            if k == bad:
                del obj[k]
            else:
                scrub(obj[k], bad)
    elif isinstance(obj, list):
        for i in reversed(range(len(obj))):
            if obj[i] == bad:
                del obj[i]
            else:
                scrub(obj[i], bad)

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

作为旁注:从不写一个裸的except子句。从来没有永远。这应该是非法的语法,真的。

答案 1 :(得分: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}}}

答案 2 :(得分:0)

这里是@bruno desthuilliers之一的通用版本,带有callable以测试密钥。

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)