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

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

理解yield:代替return的yield

簡單來講,yield是代替return的另外一種方案:服務器

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

個人定義:yield在循環中代替return,每次循環返回一次值,而不是所有循環完了才返回值。多線程

yield怎麼念?併發

return咱們念「返回xx值」,我建議:yield能夠更形象的念爲"嘔吐出xx值「,每次嘔一點。

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

因此這裏確定是要並行處理的。除了傳統的多線程多進程外,咱們還能夠選擇Generator生成器,也就是由yield代替return,每次循環都返回值,而不是所有循環完了才返回結果。函數

這樣作的好處就是——極大的節省了內存。若是用return,那麼循環中的全部數據都要不斷累計到內存裏直到循環結束,這個不友好。
而yield則是一次一次的返回結果,就不會在內存裏累加了。因此數據量越大,優點就越明顯。高併發

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

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出的值不是直接由變量來,而是由「另外一個」函數得來了。

相關文章
相關標籤/搜索