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(..)
獲取下一個迭代的結果。
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
實際上它的本質是生成了這個東西:
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 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生成器呢?
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出的值不是直接由變量來,而是由“另一個”函數得來了。