为什么使用Python生成器遍历二叉树比不使用慢得多?

6 投票
3 回答
1422 浏览
提问于 2025-04-18 14:47

我有一个二叉树,树里的节点会处理一些数据。一开始,我用了一种标准的后序递归遍历方法。

def visit_rec(self, node, data):
    if node:
        self.visit_rec(node.left, data)
        self.visit_rec(node.right, data)

        node.do_stuff(data)

我想我可以通过使用生成器来改进这个方法,这样我就可以在其他地方也用同样的遍历方式,而不需要一直传递相同的数据。下面是这个实现的代码。

def visit_rec_gen(self, node):
    if node:
        for n in self.visit_rec_gen(node.left):
                yield n
        for n in self.visit_rec_gen(node.right):
                yield n

        yield node

for node in self.visit_rec_gen():
    node.do_stuff(data)

但是,这个新方法比之前的版本慢了很多(大约从50秒变成了17秒),而且用了更多的内存。我在生成器函数的实现上是不是犯了什么错误?我希望能用这个方法,但不想牺牲性能。

补充说明:我应该一开始就提到,这些结果是在PyPy 2.3.1下得到的,而不是标准的CPython。

3 个回答

2

生成方法的效率比较低,这是因为使用生成器的实际情况。不过,如果你使用基于回调的系统,你可以在保持生成器灵活性的同时,获得接近非生成器方法的效率。

# NOTE that this should be a method on Node, not Tree
def apply_to_children_and_self(self, func, *args, **kwargs):
    if self.left:
        self.left.apply_to_children_and_self(func, *args, **kwargs)
    if self.right:
        self.right.apply_to_children_and_self(func, *args, **kwargs)
    func(self, *args, **kwargs)

...

head.apply_to_children_and_self(Node.do_stuff, data)
3

如果你在使用python3.3,yield from 这个语句被优化得比普通的循环更快,目的是为了更高效地返回值:

def visit_rec_gen(self, node):
    if node:
        yield from self.visit_rec_gen(node.left)
        yield from self.visit_rec_gen(node.right)
        yield node
6

在PyPy中,函数调用的效率比生成器或迭代器要高得多。

在PyPy中,有很多东西的性能表现各不相同(比如,PyPy的itertools.islice()性能非常差)。

你通过测量性能来找出最快的方法,这样做是对的。

另外,PyPy有一些工具可以显示生成的代码,这样你就能更详细地了解“它是怎么做的”。当然,关于“为什么这样做”的问题,答案中也涉及到人类的因素,比如实现的方便程度或者开发者的习惯。

撰写回答