We find two main senses for the verb "to yield" in dictionaries: to produce or to give way.
Luciano Ramalho 在他的《Fluent Python》協程一節中如是寫道。yield 是一個在不少語言中都有的關鍵字和特性,和它有關的種種概念——生成器,協程……可能讓人費解,但一旦真正理解了它們的含義,一扇新的大門將爲咱們展開。node
不少時候,生成器能夠用來代替遞歸。衆所周知,遞歸實現的算法簡潔,優雅,但對於Python來講,性能不好,並且還有遞歸深度限制。固然咱們能夠把某些遞歸改寫成循環和迭代的形式,但生成器能夠幫助咱們寫出既優雅又高性能的代碼。python
咱們先來個簡單的例子——生成斐波那契數列。算法
def fib_rec(n):
if n==0 or n==1:
return 1
else:
return fib_rec(n-2) + fib_rec(n-1)
def fib_gen():
before2 = 0 #原諒變量名起的渣
before1 = 1
while True:
now = before2 + before1
yield now
before2, before1 = before1 , now複製代碼
這個例子很簡單,並且好像生成器版本的代碼也不怎麼優雅和易讀,可是理解了程序流就會以爲很好理解。express
開胃小菜事後,咱們來道可口的。設計模式
David Beazley 在他的《Python Cookbook(第三版)》中的一節中介紹瞭如何使用生成器來改寫訪問者類的遞歸版本。讓人拍案。數據結構
首先咱們看一下改寫的基礎代碼app
import types
class Node:
pass
class NodeVisitor:
def visit(self, node):
stack = [node]
last_result = None
while stack:
try:
last = stack[-1]
if isinstance(last, types.GeneratorType):
stack.append(last.send(last_result))
last_result = None
elif isinstance(last, Node):
stack.append(self._visit(stack.pop()))
else:
last_result = stack.pop()
except StopIteration:
stack.pop()
return last_result
def _visit(self, node):
methname = 'visit_' + type(node).__name__
meth = getattr(self, methname, None)
if meth is None:
meth = self.generic_visit
return meth(node)
def generic_visit(self, node):
raise RuntimeError('No {} method'.format('visit_' + type(node).__name__))複製代碼
遞歸的調用
性能
class UnaryOperator(Node):
def __init__(self, operand):
self.operand = operand
class BinaryOperator(Node):
def __init__(self, left, right):
self.left = left
self.right = right
class Add(BinaryOperator):
pass
class Sub(BinaryOperator):
pass
class Mul(BinaryOperator):
pass
class Div(BinaryOperator):
pass
class Negate(UnaryOperator):
pass
class Number(Node):
def __init__(self, value):
self.value = value
# A sample visitor class that evaluates expressions
class Evaluator(NodeVisitor):
def visit_Number(self, node):
return node.value
def visit_Add(self, node):
return self.visit(node.left) + self.visit(node.right)
def visit_Sub(self, node):
return self.visit(node.left) - self.visit(node.right)
def visit_Mul(self, node):
return self.visit(node.left) * self.visit(node.right)
def visit_Div(self, node):
return self.visit(node.left) / self.visit(node.right)
def visit_Negate(self, node):
return -self.visit(node.operand)
if __name__ == '__main__':
# 1 + 2*(3-4) / 5
t1 = Sub(Number(3), Number(4))
t2 = Mul(Number(2), t1)
t3 = Div(t2, Number(5))
t4 = Add(Number(1), t3)
# Evaluate it
e = Evaluator()
print(e.visit(t4)) # Outputs 0.6複製代碼
一旦嵌套過深,就會出現問題
lua
>>> a = Number(0)
>>> for n in range(1, 100000):
... a = Add(a, Number(n))
...
>>> e = Evaluator()
>>> e.visit(a)
Traceback (most recent call last):
...
File "visitor.py", line 29, in _visit
return meth(node)
File "visitor.py", line 67, in visit_Add
return self.visit(node.left) + self.visit(node.right)
RuntimeError: maximum recursion depth exceeded
>>>複製代碼
而咱們用生成器的方式來調用,一切又均可以運行了
spa
class Evaluator(NodeVisitor):
def visit_Number(self, node):
return node.value
def visit_Add(self, node):
yield (yield node.left) + (yield node.right)
def visit_Sub(self, node):
yield (yield node.left) - (yield node.right)
def visit_Mul(self, node):
yield (yield node.left) * (yield node.right)
def visit_Div(self, node):
yield (yield node.left) / (yield node.right)
def visit_Negate(self, node):
yield - (yield node.operand)複製代碼
>>> a = Number(0)
>>> for n in range(1,100000):
... a = Add(a, Number(n))
...
>>> e = Evaluator()
>>> e.visit(a)
4999950000
>>>複製代碼
神奇嗎?僅僅是將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的那幾個特性,只是要有想象力來充分的利用。但願咱們都能讓它們變成改善代碼的好幫手。