Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/310.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
为什么使用Python生成器遍历二叉树要慢得多?_Python_Recursion_Generator_Pypy - Fatal编程技术网

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

为什么使用Python生成器遍历二叉树要慢得多?,python,recursion,generator,pypy,Python,Recursion,Generator,Pypy,我有一个二叉树,其中节点与数据交互。我最初实现了一个标准的后序递归遍历 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(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下获得的。

如果您使用的是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

在PyPy上,函数调用比生成器或迭代器优化得多

PyPy中有许多具有不同性能特征的东西(例如,PyPy的itertools.islice()执行的非常小)

通过测量性能来确定哪种方式最快,这是正确的


另外请注意,PyPy有一些工具可以显示生成的代码,这样您就可以更详细地回答“它做什么”的问题。当然,“它为什么这样做”这个问题的答案中有一个人类因素,它涉及到什么是方便实现的或者实现者的倾向。

由于使用生成器的现实情况,生成方法的效率更低。但是,您可以使用基于回调的系统的非生成器的许多效率来获得生成器方法的灵活性

# 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)

乍一看,我注意到您的函数实际上并没有做相同的事情。生成器版本在进入函数和进行递归调用时检查每个节点,而原始函数仅在进入函数时检查节点。此外,这些方法属于哪个类?我已经检查过了,递归调用中的第二个检查是不必要的,但实际上对性能没有任何影响。我将把它从问题中删除。它们是我的BinaryTree类的一部分。难道你不想
yield self吗?请访问\u rec\u gen(node.left)
,而不是在self中访问n的
。访问\u rec\u gen(node.left):yield n
?这些函数实际上并不相同。第一个函数非常直接,只需从本地名称空间和对象中获取一些变量,然后调用自己两次。第二个,因为它使用生成器,正在构建一组可重入堆栈帧,并在它们之间不断地来回翻转。它的代码更灵活,但效率也低得多。我目前使用的是PyPy 2.3.1(它使用的是Python 2.7.6),所以我不能使用它,否则它看起来很有用。@Stu,结果与CPython类似吗?我想知道这是否是pypy特有的问题?@Stu,你也应该在问题中突出你使用pypy的事实。这一点很好。CPython(2.7)的时间分别为~25s和~35s。然而,递归生成器使用的内存比使用PyPy运行时少得多(大约是10倍)。我真的不明白在任何解释器中,生成器版本是如何更快的。从根本上讲,它更复杂。正如在另一篇评论中提到的,我重新运行了CPython(2.7)下的示例,分别得到了~25s和~35s。然而,递归生成器使用的内存比使用PyPy运行时少得多(大约10倍)。因此,是的,虽然生成器版本在CPython下也没有更快,但速度上的差异更小,而且它使用的内存也更少。@塞拉斯一般来说,生成器比函数调用做的工作更少,因为生成器只创建一次堆栈帧,并在每次调用中重复使用它。相反,CPython的函数调用往往比较慢,因为每次调用都会创建一个新的堆栈帧。在PyPy中,stackframe的大部分创建开销都被优化掉了,因此递归函数与生成器调用的速度比更有利。@RaymondHettinger我明白了,但在这个特定的例子中,这并不特别相关。与生成版本一样,非生成版本仅为每个节点创建一帧,但与生成版本不同,它不是不断地在框架树上走来走去,在所有嵌套的生成器中冒泡生成的节点。等式的另一面是PyPy在优化函数方面比生成器做得更好。这不是本质上的区别,随着PyPy的不断改进,它可能会改变。我已经尝试按原样实现了这一点,但在递归调用中得到一个错误,称“节点”不存在。如果我将其更改为“self”,则会出现一个类型错误,表示节点对象不可调用。抱歉,我在处理它时正在编辑它,但我忘了删除它们。。。应该根本没有显式传递节点。这与标准递归实现在性能方面大致相同,尽管我喜欢它的灵活性,谢谢!