Python高級語法之:一篇文章瞭解yield與Generator生成器

Python高級語法中,由一個yield關鍵詞生成的generator生成器,是精髓中的精髓。它雖然比裝飾器、魔法方法更難懂,但是它強大到我們難以想象的地步:小到簡單的for loop循環,大到代替多線程做服務器的高併發處理,都可以基於yield來實現。

理解yield:代替return的yield

簡單來說,yield是代替return的另一種方案:

  • return就像人只有一輩子,一個函數一旦return,它的生命就結束了
  • yield就像有“第二人生”、“第三人生”甚至輪迴轉世一樣,函數不但能返回值,“重生”以後還能再接着“上輩子”的記憶繼續返回值

我的定義:yield在循環中代替return,每次循環返回一次值,而不是全部循環完了才返回值。

yield怎麼念?

return我們念“返回xx值”,我建議:yield可以更形象的念爲"嘔吐出xx值“,每次嘔一點。

一般我們進行循環迭代的時候,都必須等待循環結束後才return結果。
數量小的時候還行,但是如果循環次數上百萬?上億?我們要等多久?
如果循環中不涉及I/O還行,但是如果涉及I/O堵塞,一個堵幾秒,後邊幾百萬個客戶等着呢,銀行櫃檯還能不能下班了?

所以這裏肯定是要並行處理的。除了傳統的多線程多進程外,我們還可以選擇Generator生成器,也就是由yield代替return,每次循環都返回值,而不是全部循環完了才返回結果。

這樣做的好處就是——極大的節省了內存。如果用return,那麼循環中的所有數據都要不斷累計到內存裏直到循環結束,這個不友好。
而yield則是一次一次的返回結果,就不會在內存裏累加了。所以數據量越大,優勢就越明顯。

有多明顯?如果做一百萬的簡單數字計算,普通的for loop return會增加300MB+的內存佔用!而用yield一次一次返回,增加的內存佔用幾乎爲0MB!

yield的位置

既然yield不是全部循環完了再返回,而是循環中每次都返回,所以位置自然不是在for loop之後,而是在loop之中。

先來看一般的for loop返回:

def square(numbers):
    result = []
    for n in numbers:
        result.append( n**2 )
    return result    #在for之外

再來看看yield怎麼做:

def square(numbers):
    for n in numbers:
        yield n**2    #在for之中

可以看到,yield在for loop之中,且函數完全不需要寫return返回。

這時候如果你print( square([1,2,3]) )得到的就不是直接的結果,而是一個<generator object>
如果要使用,就必須一次一次的next(...)來獲取下一個值:

>>> results = square( [1,2,3] )
>>> next( result )
1
>>> next( result )
4
>>> next( result )
9
>>> next( result )
ERROR: StopIteration

這個時候更簡單的做法是:

for r in results:
    print( r )

因爲in這個關鍵詞自動在後臺爲我們調用生成器的next(..)函數

什麼是generator生成器?
只要我們在一個函數中用了yield關鍵字,函數就會返回一個<generator object>生成器對象,兩者是相輔相成的。有了這個對象後,我們就可以使用一系列的操作來控制這個循環結果了,比如next(..)獲取下一個迭代的結果。

yieldgenerator的關係,簡單來說就是一個起因一個結果:只要寫上yield, 其所在的函數就立馬變成一個<generator object>對象。

xrange:用生成器實現的range

Python中我們使用range()函數生成數列非常常用。而xrange()的使用方法、效果幾乎一模一樣,唯一不同的就是——xrange()返回的是生成器,而不是直接的結果。
如果數據量大時,xrange()能極大的減小內存佔用,帶來卓越的性能提升。

當然,幾百、幾千的數量級,就直接用range好了。

多重yield

有時候我們可能會在一個函數中、或者一個for loop中看到多個yield,這有點不太好理解。
但其實很簡單!

一般情況下,我們寫的:

for n in [1,2,3]:
    yield n**2

實際上它的本質是生成了這個東西:

yield 1**2
yield 2**2
yield 3**2

也就是說,不用for loop,我們自己手寫一個一個的yield,效果也是一樣的。

你每次調用一次next(..),就得到一個yield後面的值。然後三個yield的第一個就會被劃掉,剩兩個。再調用一次,再劃掉一個,就剩一個。直到一個都不剩,next(..)就返回異常。
一旦瞭解這個本質,我們就能理解一個函數裏寫多個yield是什麼意思了。

更深入理解yield:作爲暫停符的yield

從多重yield延伸,我們可以開始更進一步瞭解yield到底做了些什麼了。

現在,我們不把yield看作是return的替代品了,而是把它看作是一個suspense暫停符。
即每次程序遇到yield,都會暫停。當你調用next(..)時候,它再resume繼續。

比如我們改一下上面的程序:

def func():
    yield 1**2
    print('Hi, Im A!')

    yield 2**2
    print('Hi, Im B!')

    yield 3**2
    print('Hi, Im C!')

然後我們調用這個小函數,來看看yield產生的實際效果是什麼:

>>> f = func()
>>> f
<generator object func at 0x10d36c840>

>>> next( f )
1

>>> next( f )
Hi, Im A!
4

>>> next( f )
Hi, Im B!
9

>>> next( f )
Hi, Im C!
ERROR: StopIteration

從這裏我們可以看到:

  • 第一次調用生成器的時候,yield之後的打印沒有執行。因爲程序yield這裏暫停了
  • 第二次調用生成器的時候,第一個yield之後的語句執行了,並且再次暫停在第二個yield
  • 第三次調用生成器的時候,卡在了第三個yield。
  • 第四次調用生成器的時候,最後一個yield以下的內容還是執行了,但是因爲沒有找到第四個yield,所以報錯。

所以到了這裏,如果我們能理解yield作爲暫停符的作用,就可以非常靈活的用起來了。

yield fromsub-generator子生成器

yield from是Python 3.3開始引入的新特性。
它主要作用就是:當我需要在一個生成器函數中使用另一個生成器時,可以用yield from來簡化語句。

舉例,正常情況下我們可能有這麼兩個生成器,第二個調用第一個:

def gen1():
    yield 11
    yield 22
    yield 33

def gen2():
    for g in gen1():
        yield g
    yield 44
    yield 55
    yield 66

可以看到,我們在gen2()這個生成器中調用了gen1()的結果,並把每次獲取到的結果yield轉發出去,當成自己的yield出來的值

我們把這種一個生成器中調用的另一個生成器叫做sub-generator子生成器,而這個子生成器由yield from關鍵字生成。

由於sub-generator子生成器很常用,所以Python引入了新的語法來簡化這個代碼:yield from

上面gen2()的代碼可以簡化爲:

def gen2():
    yield from gen1()
    yield 44
    yield 55
    yield 66

這樣看起來是不是更"pythonic"了呢?:)

所以只要記住:yield from只是把別人嘔吐出來的值,直接當成自己的值嘔吐出去。

遞歸+yield能產生什麼?

一般我們只是二選一:要不然遞歸,要不然for循環中yield。有時候yield就可以解決遞歸的問題,但是有時候光用yield並不能解決,還是要用遞歸。
那麼怎麼既用到遞歸,又用到yield生成器呢?

參考:Recursion using yield

def func(n):
    result = n**2
    yield result
    if n < 100:
        yield from func( result )

for x in func(100):
    print( x )

上面代碼的邏輯是:如果n小於100,那麼每次調用next(..)的時候,都得到n的乘方。下次next,會繼續對之前的結果進行乘方,直到結果超過100爲止。

我們看到代碼裏利用了yield from子生成器。因爲yield出的值不是直接由變量來,而是由“另一個”函數得來了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章