Manjusaka

Manjusaka

聊聊 Python 中生成器和協程那點事兒

寫在前面的话#

本來想這周繼續寫寫 Flask 那點破事兒的,但是想了想決定換換口味,來聊聊很不容易理解但是很重要的 Python 中的生成器和協程。

Generators 科普#

我猜大家對於生成器肯定並不陌生,但是為了能讓我愉快的繼續裝逼,我們還是用點篇幅講一下什麼是生成器吧。
比如在 Python 裡,我們想生成一個範圍 (1,100000) 的一個 list,於是我們無腦寫了如下的代碼出來

def generateList(start,stop):
	tempList=[]
	for i in range(start,stop):
		tempList.append(i)
	return tempList

注 1:這裡有同學提出了為什麼我們不直接返回 range(start,stop),Nice question,這裡涉及到一個基礎問題,range 的機制究竟是怎樣的。這就要分版本而論了,在 Python 2.x 的版本中,range(start,stop) 其實本質上是預先生成一個 list , 而 list 對象是一個 Iterator ,因此可以被 for 語句所使用。
Python 2.x 中的 range
然後在 Python 2.x 中還有一個語句叫做 xrange ,其生成的是一個 Generator 對象。
Python 2.x 中的 xrange
在 Python 3 中事情發生了一點變化,可能社區覺得 rangexrange 分裂太過蛋疼,於是將其合併,於是現在在 Python 3 中,取消了 xrange 的語法糖,然後 range 的機制也變成生成一個 Generator 而不是 list
Python 3 中的 range

但是大家考慮過一個問題麼,如果我們想生成數據量非常大,預先生成數據的行為無疑是很不明智的,這樣會耗費大量的內存。於是 Python 給我們提供了一種新的姿勢,Generator (生成器)

def generateList1(start,stop):
	for i in range(start,stop):
		yield i

if __name__=="__main__":
	c=generateList1(1,100000)
	for i in c:
		print(i)

是的,Generator 其中一個特性就是不是一次性生成數據,而是生成一個可迭代的對象,在迭代時,根據我們所寫的邏輯來控制其啟動時機。

Generator 深入#

這裡可能有一個問題,大家肯定想問 Python 開發者們不可能為了這一種使用場景而去單獨創建一個 Generator 機制吧,那麼我們 Generator 還有其餘的使用場景麼。當然,請看標題,對了嘛,Generator 另一個很大作用可以說就是當做協程使用。不過在這之前,我們要去深入的了解下 Generator 才能方便我們後面的講解。

關於 Generator 中的內建方法#

關於 Python 中可迭代對象的一點背景知識#

首先,我們來看看 Python 中的迭代過程。
在 Python 中迭代有兩個概念,一個是 Iterable ,另一個是 Iterator 。讓我們分別來看看
第 N 次首先,Iterable 近似的可以理解成為一個協議,判斷一個 Object 是否是 Iterable 的方法就是看其實現了 iter 與否,如果實現了 iter ,那麼這便可以認為是一個 Iterable 對象。空談誤國,實幹興邦,讓我們直接來看一段代碼理解下

class Counter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def next(self):  # Python 3: def __next__(self)
        if self.current > self.high:
            raise StopIteration
        else:
            self.current += 1
            return self.current - 1

if __name__ == '__main__':
	a=Counter(3,8)
	for c in a:
		print(c)

好了,讓我們來看看上面這段代碼裡發生了什麼,首先 for 語句的引用首先去判斷迭代的是 Iterable 對象還是 Iterator 對象,如果是實現了 __iter__ 方法的對象,那麼就是一個 Iterable 對象,for 循環首先調用對象的 __iter__ 方法來獲取一個 Iterator 對象。那么什麼是 Iterator 對象呢,這裡可以近似的理解為是實現了 next() 方法(注:在 Python3 中是 next 方法)。

OK,讓我們繼續回到剛剛說到的那裡,在上面的代碼中 for 語句首先判斷是一個 Iterable 對象還是 Iterator 對象,如果是 Iterable 對象那麼調用其 iter 方法來獲取一個 Iterator 對象,接著 for 循環會調用 Iterator 對象中的 next() (注:Python3 裡是 __next__) 方法來進行迭代,直到迭代過程結束拋出 StopIteration 異常。

好了,來聊聊 Generator#

讓我們先看看前面那段代碼吧:

def generateList1(start,stop):
	for i in range(start,stop):
		yield i

if __name__=="__main__":
	c=generateList1(1,100000)
	for i in generateList1:
		print(i)

首先我們要確定一點的是 Generator 其實也是一個 Iterator 對象。OK 讓我們來看看上面這段代碼,首先 for 確定 generateList1 是一個 Iterator 對象,然後開始調用 next() 方法進行進一步迭代。OK 此時你肯定想問這裡面 next() 方法是怎樣讓 generateList1 進一步往下迭代的呢?答案在於 Generator 的內建 send() 方法。我們還是來看一段代碼。

def generateList1(start,stop):
	for i in range(start,stop):
		yield i
if __name__=="__main__":
	a=generateList1(0,5)
	for i in range(0,5):
		print(a.send(None))

這裡我們應該輸出什麼?答案就是 0,1,2,3,4 ,結果上和我們用 for 循環進行運算的結果是不是一樣。好了,我們現在可以得出一個結論就是

Generator 迭代的本質就是通過內建的 next()__next__() 方法來調用內建的 send() 方法。

繼續吐槽 Generator 的內建方法#

前面我們提到一個結論

Generator 迭代的本質就是通過內建的 next()__next__() 方法來調用內建的 send() 方法。

現在我們來看個例子:

def countdown(n):
    print "Counting down from", n
    while n >= 0:
        newvalue = (yield n)
        # If a new value got sent in, reset n with it
        if newvalue is not None:
            n = newvalue
        else:
            n -= 1

if __name__=='__main__':
	c = countdown(5)
	for x in c:
    	print x
    	if x == 5:
        	c.send(3)

好了這段代碼的輸出應該是什麼?
答案是 [5,2,1,0] ,是不是很迷惑?別急,我們先來看看這段代碼的運行流程

代碼運行流程

簡而言之就是,當我們調用 send() 函數的時候,我們 send(x) 的值會發送給 newvalue 向下繼續執行直到遇到下一次 yield 的出現,然後返回值作為一個過程的結束。然後我們的 Generator 靜靜的沉睡在內存中,等待下一次的 send 來喚醒它。

注 2:有同志問:“這裡沒想明白,c.send (3) 是 相當於 yield n 返回了個 3 給 newvalue ?”,好的,nice question,其實這個問題我們看前面之前的代碼運行圖就知道, c.send(3) 首先,將 3 赋值給 newvalue ,然後程序運行剩下的代碼,直到遇到下個 yield 為止,那麼在這裡,我們運行剩下完代碼,在遇到 yiled n 之前,將 n 的值已經改變為 3 , 接著,yield n 即約等於 return 3。接著 countdown 這個 Generator 將所有變量的狀態凍結,然後靜靜的呆在內存中,等待下一次的 next__next__() 方法或者是 send() 方法的喚醒。

小貼士:我們如果直接調用 send() 的話,第一次請務必 send(None) 只有這樣一個 Generator 才算是真正被激活了。我們才能進行下一步操作。

說說關於協程#

首先關於協程的定義,我們來看一段 wiki

Coroutines are computer program components that generalize subroutines for nonpreemptive multitasking, by allowing multiple entry points for suspending and resuming execution at certain locations. Coroutines are well-suited for implementing more familiar program components such as cooperative tasks, exceptions, event loop, iterators, infinite lists and pipes.
According to Donald Knuth, the term coroutine was coined by Melvin Conway in 1958, after he applied it to construction of an assembly program.[1] The first published explanation of the coroutine appeared later, in 1963.

簡而言之,協程是比線程更為輕量的一種模型,我們可以自行控制啟動與停止的時機。在 Python 中其實沒有專門針對協程的這個概念,社區一般而言直接將 Generator 作為一種特殊的協程看待,想想,我們可以用 next__next__() 方法或者是 send() 方法喚醒我們的 Generator ,在運行完我們所規定的代碼後, Generator 返回並將其所有狀態凍結。這是不是很讓我們 Excited 呢!!

關於 Generator 的一點課後作業#

現在我們要後序遍歷二叉樹,我知道看這篇文章神犇們都能無腦寫出來的,讓我們看看代碼先:

class Node(object):
    def __init__(self, val, left, right):
        self.val = val
        self.left = left
        self.right = right

def visit_post(node):
    if node.left:
        return visit_post(node.left)
    if node.right:
        return visit_post(node.right)
    return node.val

if __name__ == '__main__':
    node = Node(-1, None, None)
    for val in range(100):
        node = Node(val, None, node)
    print(list(visit_post(node)))

但是,我們知道遞歸深度太深的話,我們要麼爆栈要麼 py 交易失敗,OK ,Generator 大法好,把你碼農平安保,還是直接看代碼:

def visit_post(node):
    if node.left:
        yield node.left
    if node.right:
        yield node.right
    yield node.val

def visit(node, visit_method):
    stack = [visit_method(node)]
    while stack:
        last = stack[-1]
        try:
            yielded = next(last)
        except StopIteration:
            stack.pop()
        else:
            if isinstance(yielded, Node):
                stack.append(visit_method(yielded))
            elif isinstance(yielded, int):
                yield yielded

if __name__ == '__main__':
    node = Node(-1, None, None)
    for val in range(100):
        node = Node(val, None, node)
    visit_generator = visit(node, visit_method=visit_post)
    print(list(visit_generator))

看起來很複雜是不是?沒事當做課後作業,大家可以在評論裡給我留言,我們一起進行一下 py 交易吧~

參考鏈接#

1.提高你的 Python: 解釋‘yield’和‘Generators(生成器)’
2.yield 大法好
3.http://my.oschina.net/1123581321/blog/160560
4.python 的迭代器為什麼一定要實現__iter__方法(關於迭代器那離,為了便於理解,我簡化了一些東西,具體可以參看這個問題的高票答案)

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。