使用yield将一个递归保持形式不变下转换成非递归
递归函数的可读性高,但是效率不高;非递归函数的效率高,但是可读性会略逊一筹。不过在python中借助yield这个工具我们可以做到鱼和熊掌得兼。
yield的一些技巧
函数内有yield的函数将被作为生成器,当函数执行到yield这里时会将生成器挂起,直到下一次调用。
yield当然还有其他的很多用处和技巧,可以说是python中最重要的概念之一。我们此处用到的技巧主要还是send()
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 def a (): yield 1 x = yield yield x b = a() print (next (b)) print (next (b)) print (b.send(2 )) def c (): yield 1 x = yield 2 yield x d = c() print (next (d)) print (next (d)) print (d.send(3 )) print (d.send(4 ))
send()
函数可以向生成器中发送数据,但是在调用send前需要保证生成器处于yield的位置上(即保证在send前调用next,当然send本身也是会有一次next调用的,因此send也会返回值 )
关于send的细节此处就不展开了。
在上面代码中的
等效于
其中x = yield
为send的接受点,即send进生成器中的数据会被发送给x。
一个递归函数
此处我们就以一个树的访问者模式遍历为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Node : def __init__ (self, data, left = None , right = None ): self.left = left self.right = right self.data = data class Visitor : def __init__ (self ): ... def visit (self, node ): if node.left is not None : self.visit(node.left) if node.right is not None : self.visit(node.right) print (node.data) t5 = Node(5 ) t4 = Node(4 ) t3 = Node(3 , t4, t5) t2 = Node(2 , t3) t1 = Node(1 ) t0 = Node(0 , t1, t2) v = Visitor v.visit(t0)
此时的前序遍历是一个递归的操作,但是递归效率不高且会爆栈。递归爆栈的原因也很简单,即它会不断保留当前的状态,直到进入到递归的最内层,并逐层向外解开求值。
每次压入栈中时会把函数涉及到的变量一起压入,这就导致了内存占用过高而爆栈。假如存在一个栈,把结果(数据、起始结点甚至是生成器)全部压进去,然后每次从中pop一个出来根据类型处理,这样就能解决爆栈问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 from types import GeneratorTypeclass Visitor : def __init__ (self ): ... def visit (self, node ): stack = [node] while stack: try : last = stack[-1 ] if isinstance (last, GeneratorType): stack.append(next (last)) elif isinstance (last, Node): stack.append(self._visit(stack.pop())) else : print (stack.pop()) except StopIteration as e: stack.pop() def _visit (self, node ): if node.left is not None : yield self._visit(node.left) if node.right is not None : yield self._visit(node.right) yield node.data
可以看到,关键的部分还是保持了递归的形式(即自身函数调用自身),但是由于生成器函数并不会直接执行函数内部代码,所以给了可以操作的空间。
可以看成是一个状态机。
一个更高级的例子
当然,上面的例子比较简单,简单的原因是:每个生成器之间不涉及到数据交换,是独立的 。我们接下来看一下需要交换数据的例子,此处我们改写一下斐波那契。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 from types import GeneratorTypeclass Number : def __init__ (self, n ): self.n = n def dec (self, x=1 ): return Number(self.n-x) class Fib : def fib (self, number ): stack = [number] last_result = None while stack: try : last = stack[-1 ] if isinstance (last, GeneratorType): t = last.send(last_result) stack.append(t) last_result = None elif isinstance (last, Number): stack.append(self._fib(stack.pop())) else : last_result = stack.pop() except StopAsyncIteration as e: stack.pop() return last_result def _fib (self, n ): if n.n == 1 or n.n == 2 : yield 1 else : yield (yield self._fib(n.dec())) + (yield self._fib(n.dec(2 ))) def fib_raw (self, n ): if n == 1 or n == 2 : return 1 else : return self.fib_raw(n-1 ) + self.fib_raw(n-2 ) f = Fib() print (f.fib(Number(6 )))print (f.fib_raw(6 ))
每次遇到生成器后都要把last_result设置为None,需要注意last_result
的作用有两个:
如果生成出来的还是生成器的话,表示递归继续,此时就send(None)让新生成的迭代器进入send位置
如果生成出来的是值,那么下次循环会把这个值设置成last_result,这个值会被send进最近的一个生成器中,这就完成了递归产生数据的注入
不过这种方式似乎需要保证中间过程值与结果输出值类型不一致呢。
其中
1 2 3 yield (yield n.dec()) + (yield n.dec(2 ))
等效于
1 2 3 4 5 x = yield n.dec() y = yield n.dec(2 ) yield x + y
注意,这种方式本质上只改善了递归的爆栈问题,它的实际运行过程跟递归是一样的。(即,不能当作递归-->非递归的优化方法)
其实就是把递归的整个过程用yield和一个栈来全部模拟一遍啦,且在模拟的时候不改变递归函数形式(即仍然是自身调用自身,但是用生成器惰性求值+挂起的特性来防止不断申请栈空间)