使用Python生成器预先遍历遍历,并使用忽略子树的机制

时间:2014-10-06 17:14:16

标签: python python-2.7 tree generator

我已编写以下代码来执行Python dict的预订遍历,其中可能包含其他dict s:

def preorder_traversal(obj):
    yield obj
    if isinstance(obj, dict):
        for k, v in obj.iteritems():
            for o in preorder_traversal(v):
                yield o

以下是一些行为示例:

> list(preorder_traversal({}))
[{}]
> list(preorder_traversal({'a': 1, 'b': 2}))
[{'a': 1, 'b': 2}, 1, 2]
> list(preorder_traversal({'a': {'c': 1}, 'b': 2}))
[{'a': {'c': 1}, 'b': 2}, {'c': 1}, 1, 2]

树可能变得非常大,所以我想添加一种机制,使预订遍历的消费者可以中止对整个子树的搜索。

这是我提出的代码,包括鼻子测试案例。测试失败,如下所述。

class IgnoreSubtree(Exception):
    pass    

def preorder_traversal(obj):
    try:
        yield obj
    except IgnoreSubtree:
        return

    if isinstance(obj, dict):
        for k, v in obj.iteritems():
            iterator = preorder_traversal(v)
            for o in iterator:
                try:
                    yield o
                except IgnoreSubtree as e:
                    try:
                        iterator.throw(e)
                    except StopIteration:  # WHY?
                        pass

def test_ignore_subtree():
    obj = {'a': {'c': 3}, 'b': 2}
    iterator = preorder_traversal(obj)
    out = []
    for o in iterator:
        out.append(o)
        if o == {'c': 3}:
            iterator.throw(IgnoreSubtree)

    eq_([obj, {'c': 3}, 2], out)

测试失败,出现以下错误:

AssertionError: [{'a': {'c': 3}, 'b': 2}, {'c': 3}, 2] !=
                [{'a': {'c': 3}, 'b': 2}, {'c': 3}]

即。 IgnoreSubtree也已经中止了顶层对象中k / v对的迭代,它还没有删除{'c': 3}子树。

这段代码有什么问题?为什么StopIteration被抛到上面的注释位置?这是为这个函数实现子树修剪的合理方法,还是有更好的方法可以解决它?

6 个答案:

答案 0 :(得分:1)

正如audiodude所提到的那样,你的iterator.throw(IgnoreSubtree)会返回iterator的下一个值(暂时掩盖复杂的异常处理),所以它消耗了你2期待在out的循环的下一次迭代中看到附加到test_ignore_subtree

您还询问了StopIteration被抛出的原因;抛出/抓住Exception s的顺序是:

  • iterator.throw(IgnoreSubtree)会引发IgnoreSubtree
  • 内环中的preorder_traversal
  • IgnoreSubtree使用iterator.throw(e)
  • 路由到内部迭代器
  • IgnoreSubtreeexcept IgnoreSubtree:捕获并且return被调用;但是,iterator.throw(e)希望从内部迭代器获取下一个值,该迭代器只有return ed,因此会引发StopIteration
  • 在原始iterator.throw(IgnoreSubtree)返回之前,它再次通过preorder_traversal中的外部循环,因为它想要从iterator返回下一个值。

我希望这有帮助!

<强>更新

以下是我将使用的这个基本方案的实现,以及通过nosetest:

from nose.tools import eq_

def preorder_traversal(obj, ignore_only_descendents_of=None, ignore_subtrees=None):
    if ignore_subtrees and obj in ignore_subtrees:
        return

    yield obj

    if ignore_only_descendents_of and obj in ignore_only_descendents_of:
        return

    if isinstance(obj, dict):
        for k, v in iter(sorted(obj.iteritems())):
            iterator = preorder_traversal(v, ignore_only_descendents_of, ignore_subtrees)
            for o in iterator:
                yield o


def test_ignore_subtree():
    obj = {'a': {'c': 3}, 'b': 2, 'd': {'e': {'f': 4}}, 'g': 5, 'h': 6}
    ignore_only_descendents_of = [{'e': {'f': 4}}]
    ignore_subtrees = [{'c': 3}, 5]

    iterator = preorder_traversal(obj, ignore_only_descendents_of, ignore_subtrees)
    out = []

    for o in iterator:
        out.append(o)

    expected = [obj, 2, {'e': {'f': 4}}, 6]
    eq_(expected, out)

注意事项:

  • 您的示例允许排除{'c':3}的后代,同时包括{'c':3}本身;我发现这有点令人困惑,因为我希望您通常希望排除包含其根的整个子树,因此我已更改preorder_traversal以采用两种可选的事物列表以各种方式排除。< / LI>
  • 将子树移动到迭代器本身似乎更清晰;您可以避免使用Exception来完全控制流量。
  • 更复杂的示例,演示了两种类型的子树排除。

答案 1 :(得分:1)

首先,为什么StopIteration被提出。您对preorder_traversal的定义始于:

try:
    yield obj
except IgnoreSubtree:
    return

在生成器中,普通return语句等同于raise StopIteration 。在python3.3 +中,您实际上可以使用return value,它等同于raise StopIteration(value)

所以,你throw遇到某个异常,它被执行return的生成器捕获,因此引发了StopIteration。每当您致电sendnextthrow时,如果生成器结束执行而未找到StopIteration,则可能会引发yield,因此您正在使用的代码在测试中,只要跳过一个子树就会结束迭代,就注定要引发StopIteration

换句话说,您的测试存在缺陷,因为throw调用可能会引发异常,即使您的生成器具有正确的实现。因此,您应该将该调用包装在try语句中:

try:
    iterator.throw(IgnoreSubtree)
except StopIteration:
    break

或者,您可以使用suppress上下文管理器来取消StopIteration

with suppress(StopIteration):
    for o in iterator:
        ...
        iterator.throw(IgnoreSubtree)

如果你没有使用python3.4,你可以使用@contextmanager装饰器轻松重新实现这个上下文管理器(从python 2.6开始就可以使用):

def suppress(*exceptions):
    try:
        yield
    except exceptions:
        pass

您的代码基本上是正确的。如果您使用的是python3.3 +,则可以将其简化为:

def preorder_traversal(obj):
    try:
        yield obj
    except IgnoreSubtree:
        return
    else:
        if isinstance(obj, dict):
            for k, v in obj.items():
                yield from preorder_traversal(v)

一旦外部StopIteration的{​​{1}}被取消,您的实施不会给我带来任何错误。结果也是你所期望的。 不幸的是,没有throw我没有看到任何简化控制流程的方法。

答案 2 :(得分:1)

我认为其他答案过于复杂。发电机是正确的!问题是 test 中的这一行:

iterator.throw(IgnoreSubtree)

相反,它应该是这样的:

out.append(iterator.throw(IgnoreSubtree))

迭代器的行为与预期的一样。但正如其他人所说,it.throw会返回下一个值。您丢弃了子树修剪后的值,因为您没有在测试中保存throw的结果!在发送StopIteration完全结束迭代器的情况下,您实际上还需要捕获IgnoreSubtree。但它看起来像我不需要其他更改。

以下是显示差异的代码:

def test_ignore_subtree(obj, halt, expected):
    iterator = preorder_traversal(obj)
    out = []
    for o in iterator:
        out.append(o)
        if o == halt:
            out.append(iterator.throw(IgnoreSubtree))

    print expected
    print out

def test_ignore_subtree_wrong(obj, halt, expected):
    iterator = preorder_traversal(obj)
    out = []
    for o in iterator:
        out.append(o)
        if o == halt:
            iterator.throw(IgnoreSubtree)

    print expected
    print out

print "Test 1"
obj = {'a': {'c': 3}, 'b': 2}
halt = {'c': 3}
expected = [obj, {'c': 3}, 2]
test_ignore_subtree(obj, halt, expected)
test_ignore_subtree_wrong(obj, halt, expected)
print "Test 2"
obj = {'a': {'c': 3, 'd': 4}, 'b': 6, 'c': 5, 'd': 7}
halt = 3
expected = [obj, {'c': 3, 'd': 4}, 3, 5, 6, 7]
test_ignore_subtree(obj, halt, expected)
test_ignore_subtree_wrong(obj, halt, expected)
print "Test 3"
obj = {'a': {'c': 3, 'd': 4}, 'b': 2}
halt = 3
expected = [obj, {'c': 3, 'd': 4}, 3, 2]
test_ignore_subtree(obj, halt, expected)
test_ignore_subtree_wrong(obj, halt, expected)
print "Test 4"
obj = {'a': {'c': 3, 'd': 4}, 'b': 2, 'c': 5, 'd': 7}
halt = 3
expected = [obj, {'c': 3, 'd': 4}, 3, 5, 2, 7]
test_ignore_subtree(obj, halt, expected)
test_ignore_subtree_wrong(obj, halt, expected)

输出(注意所有&#34;错误&#34;输出后,树的修剪部分之后的第一个值是如何丢失的:

Test 1
[{'a': {'c': 3}, 'b': 2}, {'c': 3}, 2]
[{'a': {'c': 3}, 'b': 2}, {'c': 3}, 2]
[{'a': {'c': 3}, 'b': 2}, {'c': 3}, 2]
[{'a': {'c': 3}, 'b': 2}, {'c': 3}]
Test 2
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 6, 'd': 7}, {'c': 3, 'd': 4}, 3, 5, 6, 7]
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 6, 'd': 7}, {'c': 3, 'd': 4}, 3, 5, 6, 7]
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 6, 'd': 7}, {'c': 3, 'd': 4}, 3, 5, 6, 7]
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 6, 'd': 7}, {'c': 3, 'd': 4}, 3, 6, 7]
Test 3
[{'a': {'c': 3, 'd': 4}, 'b': 2}, {'c': 3, 'd': 4}, 3, 2]
[{'a': {'c': 3, 'd': 4}, 'b': 2}, {'c': 3, 'd': 4}, 3, 2]
[{'a': {'c': 3, 'd': 4}, 'b': 2}, {'c': 3, 'd': 4}, 3, 2]
[{'a': {'c': 3, 'd': 4}, 'b': 2}, {'c': 3, 'd': 4}, 3]
Test 4
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 2, 'd': 7}, {'c': 3, 'd': 4}, 3, 5, 2, 7]
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 2, 'd': 7}, {'c': 3, 'd': 4}, 3, 5, 2, 7]
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 2, 'd': 7}, {'c': 3, 'd': 4}, 3, 5, 2, 7]
[{'a': {'c': 3, 'd': 4}, 'c': 5, 'b': 2, 'd': 7}, {'c': 3, 'd': 4}, 3, 2, 7]

我不得不按下预期值以使序列正确,因为字典以不可预测的顺序迭代。但我认为结果是合理的。

答案 3 :(得分:1)

通过{{1}对迭代器的异常实现子树修剪会导致生成器和使用它的函数中出现乱码,容易出错的代码。看一下这里的一些答案让我觉得过滤器回调函数是一种更为理智的方法。

这将是Ryan's answer

的概括
throw

这里有一些鼻子测试,证明它是如何使用的:

def preorder_traversal(obj, bailout_fn=None):
    yield obj
    if bailout_fn and bailout_fn(obj):
        return

    if isinstance(obj, dict):
        for k, v in obj.iteritems():
            for o in preorder_traversal(v, bailout_fn):
                yield o

答案 4 :(得分:1)

我之前从未发过第二个答案,但我认为在这种情况下它是合适的。本回答的第一部分讨论了清理生成器接口的方法。第二部分讨论何时最适合使用此修复程序,以及何时更适合将throw替换为另一个结构。

清洁界面

现在发电机存在两个关键问题。它们都不与正确性有关 - 它的行为与预期一致。它们与界面有关。因此,使用包装函数修复接口问题。

第一个问题是throw返回当前测试丢弃的重要值。因此,编写一个包装器,在调用IgnoreSubtree时返回一个不重要的值。第二个问题是当抛出IgnoreSubtree时,它有时会完全耗尽迭代器。因此,编写一个捕获StopIteration的包装器并优雅地处理它。这样做:

def ptv_wrapper(obj):
    pt = preorder_traversal(obj)
    while True:
        try:
            o = pt.next()
            while True:
                try:
                    yield o
                except IgnoreSubtree as e:
                    yield
                    o = pt.throw(e)
                else:
                    break

        except StopIteration:
            return

如果您将上述代码用作preorder_traversal的包装器,则上述代码将按原样运行。

何时使用throw;何时使用回叫;何时使用send

在这种情况下是否使用throw的问题是一个困难的问题。正如danvk指出的那样,这种基于异常的方法使用了一些非常复杂(和奇特)的技术,而额外的复杂性可能不值得。此外,对于使用控制流的异常,还有一些 little 可疑。生成器已经在内部完成(使用StopIteration)因此必须有一些理由,但是值得考虑的是理由是什么。

第一个问题是使用throw 是否会增加降低已存在的代码的复杂性。如果您的用例不涉及发电机和消费者之间的紧密耦合,那么您最好使用回调。 (如果你的代码是紧密耦合的,但不是必须的,你应该重构!)但是,在某些情况下,紧耦合是不可避免的。在这些情况下,使用throw(或send - 见下文)可能不会增加复杂性,并可能会降低复杂性。实际上,如果你在那些回调依赖于很多外部状态来完成他们需要做的事情的情况下使用回调,那么你可能正在编写具有紧密耦合低内聚力的代码 - 两个世界中最糟糕的!通过使用throwsend,您可以确保生成器和控制它的状态靠近在一起;耦合会很高,但凝聚力也会很高,这可能会导致代码不太复杂。

第二个问题是,是使用throw还是send。第二种选择应该在这里考虑,因为它是消费者向发电机发出信号的另一种方式。您可能会将send视为LBYLthrow&#39; s EAFP。这样做有助于提供关于何时使用其中一种的直觉。这取决于您是否预期频繁地在发电机和消费者之间来回传递信号。 很少抛出异常的EAFP代码通常比相应的LBYL代码更快。但是经常抛出异常的EAFP代码将比相应的LBYL代码慢得多。这有助于解释为什么Python迭代器使用StopIterator而不是测试:在绝大多数情况下,StopIterator只会被抛出一次!因此,捕获和抛出的成本成为固定开销,很快就被其他性能瓶颈所淹没。

这意味着,如果您使用IgnoreSubtree的次数很少(更像是Python使用StopIterator),那么您可能有理由使用throw。否则,请考虑使用send

答案 5 :(得分:0)

我会指导您使用generator.throw的文档: https://docs.python.org/2/reference/expressions.html#generator.throw

引用:

  

在生成器暂停时引发类型类型的异常,并返回生成器函数产生的下一个值。如果生成器退出而没有产生另一个值,则引发StopIteration异常。

没有办法去修剪&#34;使用generator.throw的{'c': 3}子树,因为在您可以与它进行比较时已经生成了该值。另外,generator.throw的文档告诉你它试图产生一个&#34; final&#34;可以这么说,如果没有产生最终价值,它会引发StopIteration。