Python Yield Generator 詳解
(點擊
上方藍字
,快速關注我們)
來源:xybaby
www.cnblogs.com/xybaby/p/6322376.html
如有好文章投稿,請點擊 → 這裡了解詳情
本文將由淺入深詳細介紹yield以及generator,包括以下內容:什麼generator,生成generator的方法,generator的特點,generator基礎及高級應用場景,generator使用中的注意事項。本文不包括enhanced generator即pep342相關內容。
generator基礎
在python的函數(function)定義中,只要出現了yield表達式(Yield expression),那麼事實上定義的是一個generator function, 調用這個generator function返回值是一個generator。這根普通的函數調用有所區別,For example:
def gen_generator
()
:
yield
1
def gen_value
()
:
return
1
if
__name__
==
"__main__"
:
ret
=
gen_generator
()
ret
,
type
(
ret
)
#<generator object gen_generator at 0x02645648> <type "generator">
ret
=
gen_value
()
ret
,
type
(
ret
)
# 1 <type "int">
從上面的代碼可以看出,gen_generator函數返回的是一個generator實例,generator有以下特別:
遵循迭代器(iterator)協議,迭代器協議需要實現__iter__、next介面
能過多次進入、多次返回,能夠暫停函數體中代碼的執行
下面看一下測試代碼:
>>>
def
gen_example
()
:
...
"before any yield"
...
yield
"first yield"
...
"between yields"
...
yield
"second yield"
...
"no yield anymore"
...
>>>
gen
=
gen_example
()
>>>
gen
.
next
()
#第一次調用
next
before any
yield
"first yield"
>>>
gen
.
next
()
#第二次調用
next
between
yields
"second yield"
>>>
gen
.
next
()
#第三次調用
next
no
yield
anymore
Traceback
(
most recent call
last
)
:
File
"<stdin>"
,
line
1
,
in
<
module
>
StopIteratio
調用gen example方法並沒有輸出任何內容,說明函數體的代碼尚未開始執行。當調用generator的next方法,generator會執行到yield 表達式處,返回yield表達式的內容,然後暫停(掛起)在這個地方,所以第一次調用next列印第一句並返回「first yield」。 暫停意味著方法的局部變數,指針信息,運行環境都保存起來,直到下一次調用next方法恢復。第二次調用next之後就暫停在最後一個yield,再次調用next()方法,則會拋出StopIteration異常。
因為for語句能自動捕獲StopIteration異常,所以generator(本質上是任何iterator)較為常用的方法是在循環中使用:
def generator_example
()
:
yield
1
yield
2
if
__name__
==
"__main__"
:
for
e
in
generator_example
()
:
e
# output 1 2
generator function產生的generator與普通的function有什麼區別呢?
function每次都是從第一行開始運行,而generator從上一次yield開始的地方運行
function調用一次返回一個(一組)值,而generator可以多次返回
function可以被無數次重複調用,而一個generator實例在yield最後一個值 或者return之後就不能繼續調用了
在函數中使用Yield,然後調用該函數是生成generator的一種方式。另一種常見的方式是使用generator expression,For example:
>>>
gen
=
(
x
*
x
for
x
in
xrange
(
5
))
>>>
gen
<
generator object
<
genexpr
>
at
0x02655710
>
generator應用
generator基礎應用
為什麼使用generator呢,最重要的原因是可以按需生成並「返回」結果,而不是一次性產生所有的返回值,況且有時候根本就不知道「所有的返回值」。比如對於下面的代碼:
RANGE_NUM
=
100
for
i
in
[
x*
x
for
x
in
range
(
RANGE_NUM
)]
:
# 第一種方法:對列表進行迭代
# do sth for example
i
for
i
in
(
x*
x
for
x
in
range
(
RANGE_NUM
))
:
# 第二種方法:對generator進行迭代
# do sth for example
i
在上面的代碼中,兩個for語句輸出是一樣的,代碼字面上看來也就是中括弧與小括弧的區別。但這點區別差異是很大的,第一種方法返回值是一個列表,第二個方法返回的是一個generator對象。隨著RANGE_NUM的變大,第一種方法返回的列表也越大,佔用的內存也越大;但是對於第二種方法沒有任何區別。
我們再來看一個可以「返回」無窮多次的例子:
def fib
()
:
a
,
b
=
1
,
1
while
True
:
yield
a
a
,
b
=
b
,
a
+
b
這個generator擁有生成無數多「返回值」的能力,使用者可以自己決定什麼時候停止迭代。
generator高級應用
使用場景一:
Generator可用於產生數據流, generator並不立刻產生返回值,而是等到被需要的時候才會產生返回值,相當於一個主動拉取的過程(pull),比如現在有一個日誌文件,每行產生一條記錄,對於每一條記錄,不同部門的人可能處理方式不同,但是我們可以提供一個公用的、按需生成的數據流。
def gen_data_from_file
(
file_name
)
:
for
line
in
file
(
file_name
)
:
yield line
def gen_words
(
line
)
:
for
word
in
(
w
for
w
in
line
.
split
()
if
w
.
strip
())
:
yield
word
def count_words
(
file_name
)
:
word_map
=
{}
for
line
in
gen_data_from_file
(
file_name
)
:
for
word
in
gen_words
(
line
)
:
if
word
not
in
word_map
:
word_map
[
word
]
=
0
word_map
[
word
]
+=
1
return
word_map
def count_total_chars
(
file_name
)
:
total
=
0
for
line
in
gen_data_from_file
(
file_name
)
:
total
+=
len
(
line
)
return
total
if
__name__
==
"__main__"
:
print count_words
(
"test.txt"
),
count_total_chars
(
"test.txt"
)
上面的例子來自08年的PyCon一個講座。gen_words gen_data_from_file是數據生產者,而count_words count_total_chars是數據的消費者。可以看到,
數據只有在需要的時候去拉取的,而不是提前準備好。
另外gen_words中 (w for w in line.split() if w.strip()) 也是產生了一個generator。使用場景二:
一些編程場景中,一件事情可能需要執行一部分邏輯,然後等待一段時間、或者等待某個非同步的結果、或者等待某個狀態,然後繼續執行另一部分邏輯。比如微服務架構中,服務A執行了一段邏輯之後,去服務B請求一些數據,然後在服務A上繼續執行。或者在遊戲編程中,一個技能分成分多段,先執行一部分動作(效果),然後等待一段時間,然後再繼續。對於這種需要等待、而又不希望阻塞的情況,我們一般使用回調(callback)的方式。下面舉一個簡單的例子:
def
do
(
a
)
:
"do"
,
a
CallBackMgr
.
callback
(
5
,
lambda
a
=
a
:
post_do
(
a
))
def post_do
(
a
)
:
"post_do"
,
a
這裡的CallBackMgr註冊了一個5s後的時間,5s之後再調用lambda函數,可見
一段邏輯被分裂到兩個函數,而且還需要上下文的傳遞(如這裡的參數a)
。我們用yield來修改一下這個例子,yield返回值代表等待的時間。
@
yield_dec
def
do
(
a
)
:
"do"
,
a
yield
5
"post_do"
,
a
這裡需要實現一個YieldManager, 通過yield_dec這個decrator將do這個generator註冊到YieldManager,並在5s後調用next方法。Yield版本實現了和回調一樣的功能,但是看起來要清晰許多。下面給出一個簡單的實現以供參考:
# -*- coding:utf-8 -*-
import
sys
# import Timer
import types
import time
class
YieldManager
(
object
)
:
def __init__
(
self
,
tick_delta
=
0.01
)
:
self
.
generator_dict
=
{}
# self._tick_timer = Timer.addRepeatTimer(tick_delta, lambda: self.tick())
def tick
(
self
)
:
cur
=
time
.
time
()
for
gene
,
t
in
self
.
generator_dict
.
items
()
:
if
cur
>=
t
:
self
.
_do_resume_genetator
(
gene
,
cur
)
def _do_resume_genetator
(
self
,
gene
,
cur
)
:
try
:
self
.
on_generator_excute
(
gene
,
cur
)
except
StopIteration
,
e
:
self
.
remove_generator
(
gene
)
except
Exception
,
e
:
"unexcepet error"
,
type
(
e
)
self
.
remove_generator
(
gene
)
def add_generator
(
self
,
gen
,
deadline
)
:
self
.
generator_dict
[
gen
]
=
deadline
def remove_generator
(
self
,
gene
)
:
del
self
.
generator_dict
[
gene
]
def on_generator_excute
(
self
,
gen
,
cur_time
=
None
)
:
t
=
gen
.
next
()
cur_time
=
cur_time
or
time
.
time
()
self
.
add_generator
(
gen
,
t
+
cur_time
)
g_yield_mgr
=
YieldManager
()
def yield_dec
(
func
)
:
def _inner_func
(
*
args
,
**
kwargs
)
:
gen
=
func
(
*
args
,
**
kwargs
)
if
type
(
gen
)
is
types
.
GeneratorType
:
g_yield_mgr
.
on_generator_excute
(
gen
)
return
gen
return
_inner
_
func
@
yield_dec
def
do
(
a
)
:
"do"
,
a
yield
2.5
"post_do"
,
a
yield
3
"post_do again"
,
a
if
__name__
==
"__main__"
:
do
(
1
)
for
i
in
range
(
1
,
10
)
:
"simulate a timer, %s seconds passed"
%
i
time
.
sleep
(
1
)
g_yield_mgr
.
tick
()
注意事項:
(1)Yield是不能嵌套的!
def visit
(
data
)
:
for
elem
in
data
:
if
isinstance
(
elem
,
tuple
)
or
isinstance
(
elem
,
list
)
:
visit
(
elem
)
# here value retuened is generator
else
:
yield elem
if
__name__
==
"__main__"
:
for
e
in
visit
([
1
,
2
,
(
3
,
4
),
5
])
:
e
上面的代碼訪問嵌套序列裡面的每一個元素,我們期望的輸出是1 2 3 4 5,而實際輸出是1 2 5 。為什麼呢,如注釋所示,visit是一個generator function,所以第4行返回的是generator object,而代碼也沒這個generator實例迭代。那麼改改代碼,對這個臨時的generator 進行迭代就行了。
def visit
(
data
)
:
for
elem
in
data
:
if
isinstance
(
elem
,
tuple
)
or
isinstance
(
elem
,
list
)
:
for
e
in
visit
(
elem
)
:
yield
e
else
:
yield
elem
或者在python3.3中 可以使用yield from,這個語法是在pep380加入的:
def visit
(
data
)
:
for
elem
in
data
:
if
isinstance
(
elem
,
tuple
)
or
isinstance
(
elem
,
list
)
:
yield from visit
(
elem
)
else
:
yield
elem
(2)generator function中使用return
在python doc中,明確提到是可以使用return的,當generator執行到這裡的時候拋出StopIteration異常。
def gen_with_return
(
range_num
)
:
if
range_num
<
0
:
return
else
:
for
i
in
xrange
(
range_num
)
:
yield
i
if
__name__
==
"__main__"
:
print list
(
gen_with_return
(
-
1
))
print list
(
gen_with_return
(
1
))
但是,generator function中的return是不能帶任何返回值的。
def gen_with_return
(
range_num
)
:
if
range_num
<
0
:
return
0
else
:
for
i
in
xrange
(
range_num
)
:
yield
i
上面的代碼會報錯:SyntaxError: 『return』 with argument inside generator
參考
http://www.dabeaz.com/generators-uk/
https://www.python.org/dev/peps/pep-0380/
http://stackoverflow.com/questions/231767/what-does-the-yield-keyword-do
http://stackoverflow.com/questions/15809296/python-syntaxerror-return-with-argument-inside-generator
看完本文有收穫?請轉
發分享給更多人
關注「P
ython開發者」,提升Python技能
※Python Decorator 基礎
※谷歌工程師的 TensorFlow 成長之路
※基於協程的 Python 網路庫 gevent 介紹
※從「猿」到「金剛」,機器學習讓你在職業生涯超進化!
TAG:Python開發者 |
※Python super 詳解
※Python中使用Type hinting 和 annotations
※Bayesian Personalized Ranking 演算法解析及Python實現
※Python之tworoutine
※Python幫助Youtube打敗了Google Video
※Python 標準庫精華: collections.Counter
※可以拋棄 Python?Google 開源 Swift for TensorFlow 意味什麼
※如何用 Python 寫 Alfred Workflow
※在Python中使用Elasticsearch
※使用 Python的urlliib.parse 庫解析 URL
※Python 模塊 urllib.parse
※為什麼Python如此火?Why Python is so popular?
※selenium+python點擊 display:none元素解決方法匯總
※基於Python Selenium Unittest PO設計模式詳解
※可以拋棄 Python 了?Google 開源 Swift for TensorFlow 意味著什麼
※Python 特殊函數(lambda,map,filter,reduce)
※Python 的 ChatOps 庫:Opsdroid 和 Errbot
※跳一跳工具py輔助 Python提示No module named sklearn 解決方法
※Python模塊——contextlib和urllib
※Python的"print「函數在」Hello World"之外的延伸