談談 Python 的生成器
點擊上方「
Python開發
」,選擇「置頂公眾號」
關鍵時刻,第一時間送達!
第一次看到Python代碼中出現yield關鍵字時,一臉懵逼,完全理解不了這個。網上查下解釋,函數中出現了yield關鍵字,則調用該函數時會返回一個生成器。那到底什麼是生成器呢?我們經常看到類似下面的代碼
def
count
(
n
)
:
x
=
0
while
x
<
n
:
yield
x
x
+=
1
for
i
in
count
(
5
)
:
i
這段代碼執行後列印序列0到4,所以我一開始以為這個生成器就是生成一個序列呀。那這跟迭代器有什麼區別呢?我們來看下迭代器的例子:
class
CountIter
:
def
__init__
(
self
,
n
)
:
self
.
n
=
n
def
__iter__
(
self
)
:
self
.
x
= -
1
return
self
def
next
(
self
)
:
# For Python 2.x
self
.
x
+=
1
if
self
.
x
<
self
.
n
:
return
self
.
x
else
:
raise
StopIteration
for
i
in
CountIter
(
5
)
:
i
CountIter類就是一個迭代器,它的__iter__()方法返回可迭代對象,next()方法則執行下一輪迭代(註:在Python 3.x里是__next__()方法)。上面的代碼執行後也會列印序列0到4,看上去跟之前的生成器效果一樣,就是代碼長一點。不僅如此,生成器自帶next()方法,而且在越界時也會拋出StopIteration異常。
gen
=
count
(
2
)
gen
.
next
()
# 0
gen
.
next
()
# 1
gen
.
next
()
# StopIteration
那區別到底是什麼,在何種情況下,我們應該使用生成器呢?
每次執行迭代器的next()方法並返回後,該方法的上下文環境即消失了,也就是所有在next()方法中定義的局部變數就無法被訪問了。而對於生成器,每次執行next()方法後,代碼會執行到yield關鍵字處,並將yield後的參數值返回,同時當前生成器函數的上下文會被保留下來。也就是函數內所有變數的狀態會被保留,同時函數代碼執行到的位置會被保留,感覺就像函數被暫停了一樣。當再一次調用next()方法時,代碼會從yield關鍵字的下一行開始執行。很神奇吧!如果執行next()時沒有遇到yield關鍵字即退出(或返回),則拋出StopIteration異常。
本文的第一個例子是使用生成器函數來構造生成器,Python也提供了生成器表達式,下面的例子也可以列印序列0到4。
gen
=
(
x
for
x
in
range
(
5
))
# 注意這裡是(),而不是[]
for
i
in
gen
:
i
到目前為止,我們了解了生成器同迭代器在實現機制上的不同,但似乎功能是一樣的,那生成器的存在有什麼價值呢?我們先來看看除了next()方法外,生成器還提供了哪些方法。
1. close()方法
顧名思義,close()方法就是關閉生成器。生成器被關閉後,再次調用next()方法,不管能否遇到yield關鍵字,都會立即拋出StopIteration異常。
gen
=
(
x
for
x
in
range
(
5
))
gen
.
close
()
gen
.
next
()
# StopIteration
2. send()方法
這是我認為生成器最重要的功能,我們可以通過send()方法,向生成器內部傳遞參數。我們來看個例子:
def
count
(
n
)
:
x
=
0
while
x
<
n
:
value
=
yield
x
if
value
is
not
None
:
"Received value: %s"
%
value
x
+=
1
還是之前的count函數,唯一的區別是我們將」yield x」的值賦給了變數value,並將其列印出來。如何給value傳值呢?
gen
=
count
(
5
)
gen
.
next
()
# print 0
gen
.
send
(
"Hello"
)
# Received value: Hello, then print 1
我們先調用next()方法,讓代碼執行到yield關鍵字(這步必須要),當前列印出0。然後當我們調用」gen.send(『Hello』)」時,字元串』Hello』就被傳入生成器中,並作為yield關鍵字的執行結果賦給變數」value」,所以控制台會列印出」Received value: Hello」。然後代碼繼續執行,直到下一次遇到yield關鍵字後暫定,此時生成器返回的是1。
簡單的說,send()就是next()的功能,加上傳值給yield。如果你有興趣看下Python的源碼,你會發現,其實next()的實現,就是send(None)。
3. throw()方法
除了向生成器函數內部傳遞參數,我們還可以傳遞異常。還是先看例子:
def
throw_gen
()
:
try
:
yield
"Normal"
except
ValueError
:
yield
"Error"
finally
:
"Finally"
gen
=
throw_gen
()
gen
.
next
()
# Normal
gen
.
next
()
# Finally, then StopIteration
如果像往常一樣調用next()方法,會返回』Normal』。再次調用next(),會進入finally語句,列印』Finally』,同時由於函數退出,生成器會拋出StopIteration異常。我們換個方式,在第一次調用next()方法後,調用throw()方法,情況會怎樣?
gen
=
throw_gen
()
gen
.
next
()
# Normal
gen
.
throw
(
ValueError
)
# Error
gen
.
next
()
# Finally, then StopIteration
我們會看到,throw()方法向生成器函數內部傳遞了」ValueError」異常,代碼進入」except ValueError」語句,當遇到下一個yield時才暫停並退出,此時生成器返回的是』Error』字元串。簡單的說,throw()就是next()的功能,加上傳異常給yield。
聊到這裡,相信大家對生成器的功能已經有了一個很好的理解。生成器不但可以逐步生成序列,不用像列表一樣初始化時就要開闢所有的空間。它更大的價值,我個人認為,就是模擬並發。很多朋友可能已經知道,Python雖然可以支持多線程,但由於GIL(全局解釋鎖,Global Interpreter Lock)的存在,同一個時間,只能有一個線程在運行,所以無法實現真正的並發。我們暫且不討論GIL存在的意義,這裡我們提出了一個新的概念,就是協程(Coroutine)。
Python實現協程最簡單的方法,就是使用yield。當一個函數在執行過程中被阻塞時,就用yield掛起,然後執行另一個函數。當阻塞結束後,可以用next()或者send()喚醒。相比多線程,協程的好處是它在一個線程內執行,避免線程之間切換帶來的額外開銷,而且多線程中使用共享資源,往往需要加鎖,而協程不需要,因為代碼的執行順序是你完全可以預見的,不存在多個線程同時寫某個共享變數而導致出錯的情況。
我們來使用協程寫一個生產者消費者的例子:
def
consumer
()
:
last
=
""
while
True
:
receival
=
yield
last
if
receival
is
not
None
:
"Consume %s"
%
receival
last
=
receival
def
producer
(
gen
,
n
)
:
gen
.
next
()
x
=
0
while
x
<
n
:
x
+=
1
"Produce %s"
%
x
last
=
gen
.
send
(
x
)
gen
.
close
()
gen
=
consumer
()
producer
(
gen
,
5
)
執行下例子,你會看到控制台交替列印出生產和消費的結果。消費者consumer()函數是一個生成器函數,每次執行到yield時即掛起,並返回上一次的結果給生產者。生產者producer()接收到生成器的返回,並生成一個新的值,通過send()方法發送給消費者。至此,我們成功實現了一個(偽)並發。
本文中的示例代碼可以在這裡下載(http://python.jobbole.com/downloads/201608/python-yield.tar.gz)。
來源:思誠之道
www.bjhee.com/python-yield.html
Python開發整理髮布,轉載請聯繫作者獲得授權
【點擊成為Java大神】


TAG:Python開發 |