yield全面總結

dwzb發表於2018-03-13

本文首發於知乎
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處停止,等待下一次nextnext次數超出限制就會丟擲異常

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,用於初始化生成器

  1. 整個程式執行細節非常重要,詳細拆解如下
  • 第一步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程式設計

專欄目錄:目錄

版本說明:軟體及包版本說明

相關文章