当前位置:   article > 正文

神奇的yield

yield (yield node.left) + (yield node.right)
We find two main senses for the verb "to yield" in dictionaries: to produce or to give way.

Luciano Ramalho 在他的《Fluent Python》协程一节中如是写道。yield 是一个在很多语言中都有的关键字和特性,和它有关的种种概念——生成器,协程……可能让人费解,但一旦真正理解了它们的含义,一扇新的大门将为我们展开。

代替递归

很多时候,生成器可以用来代替递归。众所周知,递归实现的算法简洁,优雅,但对于Python来说,性能很差,而且还有递归深度限制。当然我们可以把某些递归改写成循环和迭代的形式,但生成器可以帮助我们写出既优雅又高性能的代码。

我们先来个简单的例子——生成斐波那契数列。

  1. def fib_rec(n):
  2. if n==0 or n==1:
  3. return 1
  4. else:
  5. return fib_rec(n-2) + fib_rec(n-1)
  6. def fib_gen():
  7. before2 = 0 #原谅变量名起的渣
  8. before1 = 1
  9. while True:
  10. now = before2 + before1
  11. yield now
  12. before2, before1 = before1 , now复制代码

这个例子很简单,而且好像生成器版本的代码也不怎么优雅和易读,但是理解了程序流就会觉得很好理解。

开胃小菜过后,我们来道可口的。

David Beazley 在他的《Python Cookbook(第三版)》中的一节中介绍了如何使用生成器来改写访问者类的递归版本。让人拍案。

首先我们看一下改写的基础代码

  1. import types
  2. class Node:
  3. pass
  4. class NodeVisitor:
  5. def visit(self, node):
  6. stack = [node]
  7. last_result = None
  8. while stack:
  9. try:
  10. last = stack[-1]
  11. if isinstance(last, types.GeneratorType):
  12. stack.append(last.send(last_result))
  13. last_result = None
  14. elif isinstance(last, Node):
  15. stack.append(self._visit(stack.pop()))
  16. else:
  17. last_result = stack.pop()
  18. except StopIteration:
  19. stack.pop()
  20. return last_result
  21. def _visit(self, node):
  22. methname = 'visit_' + type(node).__name__
  23. meth = getattr(self, methname, None)
  24. if meth is None:
  25. meth = self.generic_visit
  26. return meth(node)
  27. def generic_visit(self, node):
  28. raise RuntimeError('No {} method'.format('visit_' + type(node).__name__))复制代码

递归的调用

  1. class UnaryOperator(Node):
  2. def __init__(self, operand):
  3. self.operand = operand
  4. class BinaryOperator(Node):
  5. def __init__(self, left, right):
  6. self.left = left
  7. self.right = right
  8. class Add(BinaryOperator):
  9. pass
  10. class Sub(BinaryOperator):
  11. pass
  12. class Mul(BinaryOperator):
  13. pass
  14. class Div(BinaryOperator):
  15. pass
  16. class Negate(UnaryOperator):
  17. pass
  18. class Number(Node):
  19. def __init__(self, value):
  20. self.value = value
  21. # A sample visitor class that evaluates expressions
  22. class Evaluator(NodeVisitor):
  23. def visit_Number(self, node):
  24. return node.value
  25. def visit_Add(self, node):
  26. return self.visit(node.left) + self.visit(node.right)
  27. def visit_Sub(self, node):
  28. return self.visit(node.left) - self.visit(node.right)
  29. def visit_Mul(self, node):
  30. return self.visit(node.left) * self.visit(node.right)
  31. def visit_Div(self, node):
  32. return self.visit(node.left) / self.visit(node.right)
  33. def visit_Negate(self, node):
  34. return -self.visit(node.operand)
  35. if __name__ == '__main__':
  36. # 1 + 2*(3-4) / 5
  37. t1 = Sub(Number(3), Number(4))
  38. t2 = Mul(Number(2), t1)
  39. t3 = Div(t2, Number(5))
  40. t4 = Add(Number(1), t3)
  41. # Evaluate it
  42. e = Evaluator()
  43. print(e.visit(t4)) # Outputs 0.6复制代码

一旦嵌套过深,就会出现问题

  1. >>> a = Number(0)
  2. >>> for n in range(1, 100000):
  3. ... a = Add(a, Number(n))
  4. ...
  5. >>> e = Evaluator()
  6. >>> e.visit(a)
  7. Traceback (most recent call last):
  8. ...
  9. File "visitor.py", line 29, in _visit
  10. return meth(node)
  11. File "visitor.py", line 67, in visit_Add
  12. return self.visit(node.left) + self.visit(node.right)
  13. RuntimeError: maximum recursion depth exceeded
  14. >>>复制代码

而我们用生成器的方式来调用,一切又都可以运行了

  1. class Evaluator(NodeVisitor):
  2. def visit_Number(self, node):
  3. return node.value
  4. def visit_Add(self, node):
  5. yield (yield node.left) + (yield node.right)
  6. def visit_Sub(self, node):
  7. yield (yield node.left) - (yield node.right)
  8. def visit_Mul(self, node):
  9. yield (yield node.left) * (yield node.right)
  10. def visit_Div(self, node):
  11. yield (yield node.left) / (yield node.right)
  12. def visit_Negate(self, node):
  13. yield - (yield node.operand)复制代码

  1. >>> a = Number(0)
  2. >>> for n in range(1,100000):
  3. ... a = Add(a, Number(n))
  4. ...
  5. >>> e = Evaluator()
  6. >>> e.visit(a)
  7. 4999950000
  8. >>>复制代码

神奇吗?仅仅是将return换成了yield,就能有如此巨大的改变。

我们来梳理一下代码。显然,重要的地方是第一段中NodeVisitor的定义。他用一个stack来保存程序计算中的数据结构,一开始,这里保存的是一个node的实例——t4。然后调用evaluator的visit方法,取出栈顶元素——此时是t4——保存在last中。判断它是一个Node的实例,再对其调用evaluator的_visit方法,同时把它从栈中弹出。而_visit 方法基本就是一个典型的访问者的设计模式的实现。然后,我们又看到,在后几段代码中,evaluator的visit_xxx方法的实现中将return换成了yield,这意味着,它将返回一个生成器——而不是和前面的实现中递归地调用。这个生成器被追加到了stack中。这时,Nodevisitor又检查栈顶元素,是生成器,调用其send方法,参数是last_result(此时值是None)。根据evaluator的定义,它又将返回一个Node的实例,然后再把它转换为一个生成器,或者如果是一个特定的子类(这里是Number)的话,直接返回值,如此循环往复。要注意的是,如果直接返回了值,说明已经产生了一个结果,这时将它赋值给last_result(原来的值是None的哦),再由evaluator将其通过send方法传给上一个层次的生成器,如此来实现结果的传递。直至最后计算出一个总的结果,返回。

思想是什么呢?原先嵌套的调用(递归)是由python解释器来处理的。现在,我们将每一次分解转化为一个生成器保存在栈中,每次检查栈顶元素的类型来决定执行什么操作。如果是一个Node的实例,就再将其转化为生成器,或者,直接返回值。如果是数值,将其保存在last_result中,将其从栈中弹出。如果是一个生成器,调用它的send方法,参数是last_result。这样,原本面对很深的嵌套,我们可能会需要递归地调用很多次才能真正返回一个值。而现在,yield将执行权再次交还给了evaluator,告诉它先计算第一个节点,出结果之后,再计算下一个——恰好和递归的执行顺序相反(虽然代码极其相似)。而生成器依然保存着执行状态,随时等待调用。自然递归深度限制也就不会再有。

我们再来看看这个例子是如何将生成器的特性发挥的淋漓尽致的。

其实,我们已经不能把它叫成是单纯的生成器,它还用到了协程的概念。首先,就像我们开头说的,yield有两个意思——to produce or to give way 。yield (yield node.left) + (yield node.right)这一句中的yield将node返回,既是produce 也是 give way,执行权交还给了evaluator,那evaluator怎么将结果传递给生成器呢?这就是send方法的作用。send方法的参数就是生成器中yield生成的值,这句话好像有点难理解,就是说,生成器恢复执行之后,原先的yield产生的值就是send传入的参数。而生成器会执行到下一个yield处,或者raise StopIteration。这时的生成器又会产生一个值,这个值哪了呢?它就是调用send方法后返回的值。所以我们才说还用到了协程的概念,事实上,协程的逻辑和这里基本相同。

状态机

ES6向Python借鉴了列表推导的语法糖,同时,它还添加了生成器的新特性(当然不是从Python中借鉴的)。

在阮一峰的《ES6标准入门》中,他介绍了使用生成器来定义状态机,用yield来划分不同状态的技巧。我在Python书籍和社区中没有见过(可能是我孤陋寡闻)。但仔细一想,python的标准库中就有类似的用法——contextlib.contextmanager

它的用法就是使用yield来划分代码,之前的相当于上下文管理器的__enter__(),之后的相当于__exit__()。我们也可将其看作是一个状态机,只不过控制它的是python解释器。

最后

前面说的几个例子,其实也就是用了关于yield的那几个特性,只是要有想象力来充分的利用。希望我们都能让它们变成改善代码的好帮手。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/737229
推荐阅读
相关标签
  

闽ICP备14008679号