帶有 yield 的函數在 Python 中被稱之為 generator(生成器)
先記住以下結論:
(1)一個帶有 yield 的函數就是一個 generator,它和普通函數不同,生成一個 generator 看起來像函數調用,但不會執行任何函數代碼, 直到對其調用 next()(在 for 循環中會自動調用 next())才開始執行。 (2)雖然執行流程仍按函數的流程執行,但每執行到一個 yield 語句就會中斷,並返回一個迭代值,下次執行時從 yield 的下一個語句繼續執行。 (3)看起來就好像一個函數在正常執行的過程中被 yield 中斷了數次,每次中斷都會通過 yield 返回當前的迭代值。
yield 的好處是顯而易見的,把一個函數改寫為一個 generator 就獲得了迭代能力,比起用類的實例保存狀態來計算下一個 next() 的值,不僅代碼簡潔,而且執行流程異常清晰。
一:實例1:Fibonacci數列
下面是一個使用yield實現Fibonacci數列的例子:(如果使用列表來存儲的話,如果數據大了會很占用內存)
# -*- coding:utf-8 -*- def fib(max): n,a,b = 0,0,1 while n < max: yield b a,b = b,a+b n += 1 f = fib(6) print(next(f)) print(next(f)) print(next(f)) print(next(f)) print(next(f)) print(next(f))
但是,如果next的數量超過了限制的話,generator 自動拋出 StopIteration 異常,表示迭代完成。而在 for 循環里,無需處理 StopIteration 異常,循環會正常結束:
# -*- coding:utf-8 -*- def fib(max): n,a,b = 0,0,1 while n < max: yield b a,b = b,a+b n += 1 f = fib(6) for i in f: print(i)
上面這樣利用for循環寫不會報錯。
**********************第二種寫法:**********************
# -*- coding:utf-8 -*- def fib(): a = b =1 yield a yield b while 1: a , b = b , a+b yield b g = fib() # 取10個數 for i in range(10): print(next(g)) ''' # 也可以這樣寫: for num in fib(): if num > 10:break print(num) '''
實例二:用yield生成器模擬Linux中命令:tail -f file | grep python 用於查找監控日志文件中出現有python字樣的行
# 注意程序只檢測新增的日志信息! #當程序運行時,若warn.log文件中末尾有新增一行,且該一行包含python,該行就會被打印出來 #若打開warn.log時,末尾已經有了一行包含python,該行不會被打印,因為上面是f.seek(0,2)移動到了文件EOF處 #故,上面程序實現了tail -f warn.log | grep 'python'的功能,動態實時檢測warn.log中是否新增現了 #新的行,且該行包含python def tail(f): # 移動到文件的EOF最后 f.seek(0.2) while 1: # 讀取文件中新的文本行 line = f.readline() if not line:continue # yield 出每一行的數據 yield line def grep(lines,search_text): for line in lines: if search_text in line: yield line if __name__ == '__main__': flog = tail(open('log.log')) py_lines = grep(flog,'python') for line in py_lines: print(line)
實例三:有助於理解的例子
# -*- coding:utf-8 -*- def count(n): while n > 0: print('before yield') yield n n -= 1 print('after yield') g = count(5) print(g.send(None)) print(next(g)) # print(next(g)) # # print(g.__next__()) # print(g.__next__())
結果:
實例四:yield中return的作用
作為生成器,因為每次迭代就會返回一個值,所以不能顯示的在生成器函數中return 某個值,包括None值也不行,否則會拋出“SyntaxError”的異常,但是在函數中可以出現單獨的return,表示結束該語句。
通過固定長度的緩沖區不斷讀文件,防止一次性讀取出現內存溢出的例子:
def read_file(path): size = 1024 with open(path,'r') as f: while True: block = f.read(SIZE) if block: yield block else: return
實例五:如一個函數中出現多個yield則next()會停止在下一個yield前
# -*- coding:utf-8 -*- def generator(): print('one') yield 123 print('two') yield 456 print('end') g = generator() # 第一次運行,暫停在 yield 123,打印one與123 print(next(g)) # 第二次運行,暫停在 yield 456,打印two與456 print(next(g)) # 第三次運行,先打印end,但是由於后面沒有yield語句了,因此再使用next()方法會報錯 print(next(g))
實例六:關於yield的返回值與send方法
實際上,yield后面的數是我們通過next()方法取到的,也就是說123與456的結果是我們通過打印next(g)的方法獲取的!
而yield實際上也是有返回值的,看下面代碼:
# -*- coding:utf-8 -*- def generator(): print('one') a = yield 123 print(a) print('two') yield 456 print('end')
g = generator() # 第一次運行,暫停在 yield 123 print(next(g))
結果為:
one
123
然后接着加代碼:
# -*- coding:utf-8 -*- def generator(): print('one') a = yield 123 print(a) print('two') yield 456 print('end') g = generator() # 第一次運行,暫停在 yield 123 print(next(g)) # 第二次運行,暫停在 yield 456 next(g)
結果如下:
one 123 None two
這里沒有打印,直接使用的next方法,可以看到,在第二個yield與第一個結果之間多了一個None,其實,這個None就是“上一次被掛起的yield語句的返回值”,默認為None!
而我們可以通過send方法去為上一次被掛起的yield語句賦值。
看下面的例子:
# -*- coding:utf-8 -*- def my_generator(): value = yield 1 value = yield(value) value = yield(value) g = my_generator() print(next(g)) print(g.send('hello')) print(g.send('world'))
結果為:
1
hello
world
(1)當調用gen.next()方法時,python首先會執行MyGenerator方法的yield 1語句。由於是一個yield語句,因此方法的執行過程被掛起,而next方法返回值為yield關鍵字后面表達式的值,即為1。
(2)當調用gen.send('hello')方法時,python首先恢復MyGenerator方法的運行環境。同時,將表達式(yield 1)的返回值定義為send方法參數的值,即為'hello'。
這樣,接下來value=(yield 1)這一賦值語句會將value的值置為'hello'。繼續運行會遇到yield value語句。因此,MyGenerator方法再次被掛起。
同時,send方法的返回值為yield關鍵字后面表達式的值,也即value的值,為'hello'。
(3)當調用send('world')方法時MyGenerator方法的運行環境。同時,將表達式(yield value)的返回值定義為send方法參數的值,即為'world'。
這樣,接下來value=(yield value)這一賦值語句會將value的值置為'world'。第三次打印'world'。
可以看出來:第一個的next取到了1;我們把'hello'賦值給第一個yield作為其返回值,所以第二次取到的是'hello',同樣的,第三次取到的是我們為第二個yield表達式send的返回值'world'。
總的來說,send方法和next方法唯一的區別是在執行send方法會首先把上一次掛起的yield語句的返回值通過參數設定,從而實現與生成器方法的交互。
但是需要注意,在一個生成器對象沒有執行next方法之前,由於沒有yield語句被掛起,如果非要是用send方法,那么這個在第一個位置的send方法里面的參數必須是None,否則會報錯。
錯誤的寫法:
正確的寫法:
因為當send方法的參數為None時,它與next方法完全等價。但是注意,雖然上面的代碼可以接受,但是不規范。
所以,在調用send方法之前,還是先調用一次next方法為好。
實例七:利用yield實現協程——生產者消費者模型
所謂協程,可以簡單理解為函數之間相互“切換”,