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之外
<p>再來看看yield怎麽做:</p> <pre><code class="py">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
<p>這個時候更簡單的做法是:</p> <pre><code class="py">for r in results: print( r )
因為
in
這個關鍵詞自動在後臺為我們調用生成器的next(..)
函數什麽是generator生成器?
只要我們在一個函數中用了yield
關鍵字,函數就會返回一個<generator object>
生成器對象,兩者是相輔相成的。有了這個對象後,我們就可以使用一系列的操作來控制這個循環結果了,比如next(..)
獲取下一個叠代的結果。
yield
和generator
的關系,簡單來說就是一個起因一個結果:只要寫上yield, 其所在的函數就立馬變成一個<generator object>
對象。xrange:用生成器實現的range
Python中我們使用
range()
函數生成數列非常常用。而xrange()
的使用方法、效果幾乎一模一樣,唯一不同的就是——xrange()
返回的是生成器,而不是直接的結果。
如果數據量大時,xrange()
能極大的減小內存占用,帶來卓越的性能提升。當然,幾百、幾千的數量級,就直接用range好了。
多重yield
有時候我們可能會在一個函數中、或者一個for loop中看到多個
yield
,這有點不太好理解。
但其實很簡單!一般情況下,我們寫的:
for n in [1,2,3]:
yield n**2
<p>實際上它的本質是生成了這個東西:</p> <pre><code class="py">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!‘)
<p>然後我們調用這個小函數,來看看yield產生的實際效果是什麽:</p> <pre><code class="py">>>> 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 from
與sub-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出的值不是直接由變量來,而是由“另一個”函數得來了。來源:https://segmentfault.com/a/1190000018208997
Python高級語法之:一篇文章了解yield與Generator生成器