生成器(generator)中 的yield 以及幾個實例


  帶有 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實現協程——生產者消費者模型

  所謂協程,可以簡單理解為函數之間相互“切換”,

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM