本文首發於知乎
yield生成器在python中使用廣泛,更是python中協程的實現原理,有必要深入掌握。
本文分為如下幾個部分
- 簡單yield的使用
- yield空
- yield from
- send與yield賦值
- return yield
簡單yield的使用
下面是一個最簡單的yield使用的例子
def myfun(total):
for i in range(total):
yield i
複製程式碼
定義了myfun
函式,呼叫時比如myfun(4)
得到的是一個生成器,生成器有3種呼叫方式
1.用next呼叫
>>> m = myfun(4)
>>> next(m)
0
>>> next(m)
1
>>> next(m)
2
>>> next(m)
3
>>> next(m)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
複製程式碼
每次呼叫一次next
,函式都會執行到yield
處停止,等待下一次next
,next
次數超出限制就會丟擲異常
2.迴圈呼叫
for i in myfun(4):
print(i)
複製程式碼
執行結果為
0
1
2
3
複製程式碼
生成器是可迭代物件,可以用迴圈呼叫。迴圈呼叫就是最大限度地呼叫next
,並返回每次next
執行結果
這就是yield的最基本使用,下面來應用一下
python內建模組itertools中實現了一些小的生成器,我們這裡拿count
舉例子,要實現下面這樣的效果
>>> from itertools import count
>>> c = count(start = 5, step = 2)
>>> next(c)
5
>>> next(c)
7
>>> next(c)
9
>>> next(c)
11
......
複製程式碼
實現如下
def count(start, step = 1):
n = 0
while True:
yield start + n
n += step
複製程式碼
3.迴圈中呼叫next
下面迴圈中呼叫next
,用StopIteration
捕獲異常並退出迴圈
>>> m = myfun(4)
>>> while True:
... try:
... print(next(m))
... except StopIteration:
... break
...
0
1
2
3
複製程式碼
yield空
yield空相當於一箇中斷器,迴圈執行到這裡會中斷,用於輔助其他程式的執行。也可以理解成返回值是None
,我們來看下面這個例子
>>> def myfun(total):
... for i in range(total):
... print(i + 1)
... yield
...
>>> a = myfun(3)
>>> next(a)
1
>>> next(a)
2
>>> next(a)
3
>>> next(a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> a = myfun(3)
>>> for i in a:
... print(i)
...
1
None
2
None
3
None
複製程式碼
yield from
通過下面一個例子來展示yield from
的用法
def myfun(total):
for i in range(total):
yield i
yield from ['a', 'b', 'c']
複製程式碼
用next
呼叫結果如下
>>> m = myfun(3)
>>> next(m)
0
>>> next(m)
1
>>> next(m)
2
>>> next(m)
'a'
>>> next(m)
'b'
>>> next(m)
'c'
>>> next(m)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
複製程式碼
上面的函式相當於
def myfun(total):
for i in range(total):
yield i
for i in ['a', 'b', 'c']:
yield i
複製程式碼
下面我們也做一個小練習,實現itertools模組中的cycle
,效果如下
>>> from itertools import cycle
>>> a = cycle('abc')
>>> next(a)
'a'
>>> next(a)
'b'
>>> next(a)
'c'
>>> next(a)
'a'
>>> next(a)
'b'
>>> next(a)
'c'
>>> next(a)
'a'
複製程式碼
實現如下
def cycle(p):
yield from p
yield from cycle(p)
複製程式碼
send與yield賦值
先講send
,首先明確一點,next
相當於send(None)
,還是看下面最簡單的例子
>>> def myfun(total):
... for i in range(total):
... yield i
...
>>> a = myfun(3)
>>> a.send(None)
0
>>> a.send(None)
1
>>> a.send(None)
2
>>> a.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
複製程式碼
如果send()
引數不是None
呢?send()
表示向這個生成器中傳入東西,有傳入就得有變數接著,於是引出了yield
賦值,來看下面一個例子
def myfun(total):
for i in range(total):
r = yield i
print(r)
複製程式碼
執行如下
>>> a = myfun(3)
>>> a.send(1)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't send non-None value to a just-started generator
>>> a.send(None)
0
>>> a.send(1)
1
1
>>> a.send(1)
1
2
>>> a.send(1)
1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
複製程式碼
上面執行結果顯示
1.第一次send
引數必須是None
,用於初始化生成器
- 整個程式執行細節非常重要,詳細拆解如下
- 第一步
a.send(None)
,是執行第一次迴圈,執行到yield i
。注意:沒有執行r = yield
賦值這一步,這也是第一次只能傳入None
的原因 - 第二步
a.send(1)
,先r = yield
賦值,將send
進去的1賦值給了r
;然後print(r)
即第一個列印出來的1;之後進入第二次迴圈,執行到yield i
沒有賦值,把yield
的結果1列印出來,即第二個1 - 最後一次
a.send(1)
先對r
賦值,再print(r)
;然後就退出了迴圈,沒有什麼可以yield
的了,於是丟擲異常
有了send
這樣的機制,我們就可以實現函式之間的來回切換執行,這是協程的基礎。
廖雪峰老師網站上用這一特性完成了一個類似生產者消費者模式的例子,讀者可以看看根據上面的知識能不能看懂這個例子
def consumer():
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None)
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
c.close()
c = consumer()
produce(c)
複製程式碼
執行結果如下
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
複製程式碼
上面例子看起來複雜高大上,但是如果從實現上述功能上來看,實際上是走了彎路,下面的程式碼可以實現和上面一樣的功能,讀者可以細細品味
def consumer(n):
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
return '200 OK'
def produce():
n = 0
while n < 5:
n = n + 1
print('[PRODUCER] Producing %s...' % n)
r = consumer(n)
print('[PRODUCER] Consumer return: %s' % r)
produce()
複製程式碼
return yield
有時會看到return yield
的用法,其實return
只是起到終止函式的作用,先看下面這個函式
def myfun(total):
yield from range(total)
複製程式碼
>>> a = myfun(4)
>>> a
<generator object myfun at 0x000001B61CCB9CA8>
>>> for i in a:
... print(i)
...
0
1
2
3
複製程式碼
這樣a
就是個生成器。如果用return
也一樣
# 不加括號不合規定
>>> def myfun(total):
... return yield from range(total)
File "<stdin>", line 2
return yield from range(total)
^
SyntaxError: invalid syntax
>>> def myfun(total):
... return (yield from range(total))
...
>>> a = myfun(4)
>>> for i in a:
... print(i)
...
0
1
2
3
複製程式碼
不過下面兩個不一樣
def myfun1(total):
return (yield from range(total))
yield 1 # 這個1在return後面,不會再執行
def myfun2(total):
yield from range(total)
yield 1
複製程式碼
看下面執行結果
>>> a = myfun1(3)
>>> for i in a:
... print(i)
...
0
1
2
>>> a = myfun2(3)
>>> for i in a:
... print(i)
...
0
1
2
1
複製程式碼
歡迎關注我的知乎專欄
專欄主頁:python程式設計
專欄目錄:目錄
版本說明:軟體及包版本說明