神奇的yield

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的那幾個特性,只是要有想象力來充分的利用。但願咱們都能讓它們變成改善代碼的好幫手。

相關文章
相關標籤/搜索