Python3 中 Yield 理解與使用


轉自Felix文章

Python3 中 yield 對初學者一直是一個難點存在,網上很多教程,噼里啪啦寫了很多,也舉了很多例子,但是讀完之后還是不知所以然,下面問題還是不知道如何回答,內容有點多,有些地方可能有點啰嗦,但都是滿滿的干貨。

-  yield 究竟是干嘛的? - yield 是怎么執行的? - yield 的好處是什么? 

1. 迭代器與可迭代對象

開始之前,先理解一下迭代器與可迭代對象,因為 yield 其實是一種特殊的迭代器,不過這種迭代器更加優雅。

  • 可迭代對象
# 可迭代對象:列表為例
s = 'ABC'
l = list(s)
print(l)

[
'A', 'B', 'C']

 

  • 迭代器
# 迭代器對象 l1
s = 'ABC'
l = list(s)
l1 = iter(l)
print(l1)
# 取出迭代器容器中的值,沒有值后就拋出異常
print(next(l1))
print(next(l1))
print(next(l1))
print(next(l1))

 

<list_iterator object at 0x0000020D793D95C0>
A
B
C
StopIteration

 

上面案例中 l 是一個列表,是一個可迭代對象 l1 是一個迭代器,直接打印,結果是<list_iterator object="" at="" 0x0000020d793d95c0="">,訪問其中的值可以使用 for 循環或者 next 函數,所有值都被訪問后,最后會拋出 StopIteration 異常

關於迭代器與可迭代對象參考我另一篇博文,里面有詳細解釋: https://blog.csdn.net/u011318077/article/details/93754013

yield 生成器就是一個優雅的迭代器,訪問也會用到 next 函數,理解迭代器后可以更輕松的理解 yield 生成器的執行過程和原理。

2. yield 簡單案例及執行步驟

下面進入正題,如果你還沒有對 yield 有個初步分認識,那么你先把 yield 看做“return”, 這個是直觀的,它首先是個 return,普通的 return 是什么意思,就是在程序中返回某個值,返回之后程序就不再往下運行了。看做 return 之后再把它看做一個是生成器(generator)的一部分 (帶 yield 的函數才是真正的迭代器),好了,如果你對這些不明白的話,那先把 yield 看做 return,然后直接看下面的程序,你就會明白 yield 的全部意思了(只是先當做 return,本質向后看就會明白)。

  • 先看一個普通函數
# 一個普通函數:
def foo():
    print('Starting.....')
# 調用函數,直接執行語句
g = foo()
print("*" * 100)

 

Starting.....
****************************************************

 

  • 生成器函數
# 包含 yield 關鍵字,就變成了生成器函數
# 調用函數並不會執行語句
def foo():
    print('Starting.....')
    while True:
        res = yield 4
        print("res:", res)

# 下面調用函數並沒有執行,可以先將后面的語句注釋掉
# 逐行運行代碼觀察效果
g = foo()
print("第一次調用執行結果:")
print(next(g))
print("*" * 100)

print("第二次調用執行結果:")
print(next(g))
print("*" * 100)

 

第一次調用執行結果:
Starting.....
4
********************************************************************
第二次調用執行結果:
res: None
4
********************************************************************

 

  • 下面解釋代碼運行順序,相當於代碼單步調試():
  1. 程序開始執行以后,因為 foo 函數中有 yield 關鍵字,所以 foo 函數並不會真的執行, 而是先得到一個生成器 g(相當於一個對象),函數的一個狀態,函數相當於暫停了
  2. 執行第一次調用,直到遇到 next 方法,foo 函數正式開始執行,先執行 foo 函數中的 print 方法,然后進入 while 循環
  3. 程序遇到 yield 關鍵字,然后把 yield 想想成 return,return 了一個 4 之后,程序停止, 但是,程序只是返回了一個值 4,並沒有執行將 4 賦值給 res 操作,此時 next(g)語句執行完成, 所以第一次調用后的結果有兩行(第一個是 while 上面的 print 的結果,第二個是 return 出的結果) 也就是執行 print(next(g))先調用函數,最后打印出了返回值 4
  4. 程序執行 print("*" * 100),輸出 100 個*
  5. 執行第二次調用,又開始執行下面的 print(next(g)),這個時候和上面那個差不多,不過不同的是,這個時候是從剛才那個 next 程序停止的地方開始執行的,也就是要執行 res 的賦值操作語句, 這時候要注意,yield 4 返回值 4 后就停止了,並沒有賦值給前面的 res, (因為剛才那個是 return 出去了,並沒有給賦值操作的左邊傳參數),此時代碼實際是從 print("res:", res)開始執行, 這個時候 res 賦值是空,是 None,所以接着下面的輸出就是 res:None,
  6. 程序會繼續在 while 里執行,又一次碰到 yield,這個時候同樣 return 出 4,然后程序停止,print 函數輸出的 4 就是這次 return 出的 4.
  • 到這里你可能就明白 yield 和 return 的關系和區別了,帶 yield 的函數是一個生成器,而不是一個函數了,這個生成器有一個函數就是 next 函數,next 就相當於“下一步”生成哪個數,這一次的 next 開始的地方是接着上一次的 next 停止的地方執行的,所以調用 next 的時候,生成器並不會從 foo 函數的開始執行,只是接着上一步停止的地方開始,然后遇到 yield 后,return 出要生成的數,此步就結束。

總結

  • 上面的 foo()就是一個生成器函數,當一個生成器函數調用 yield,生成器函數的“狀態”會被凍結,所有的變量的值會被保留下來,下一行要執行的代碼的位置也會被記錄,就是 yield 這行代碼結束的位置直到再次調用 next()。一旦 next()再次被調用,生成器函數會從它上次離開的地方開始。如果永遠不調用 next(),yield 保存的狀態就被無視了。

  • generator 是用來產生一系列值的,yield 則像是 generator 函數的返回結果,(yield 也可以看似 return),yield 唯一所做的另一件事就是保存一個 generator 函數的狀態

  • yield 和 return 的區別,return 執行后會繼續執行后面的代碼,但是 yield 會停止之后的代碼繼續執行,注意,只是停止生成器函數內部的代碼,生成器函數外部代碼不受影響

  • generator 就是一個特殊類型的迭代器(iterator)和迭代器相似,我們可以通過使用 next()來從 generator 中獲取下一個值

3. yield 中的 send 函數

yield 生成器函數中另外一重要函數就是 send(),可以傳入一個值作為返回值,看下面案例,第二次調用時候傳入數字 7

# 包含 yield 關鍵字,就變成了生成器函數
def foo():
    print('Starting.....')
    while True:
        res = yield 4
        print("res:", res)

# 下面調用函數並沒有執行,可以先將后面的語句注釋掉
# 逐行運行代碼觀察效果
g = foo()
print("第一次調用執行結果:")
print(next(g))
print("*" * 100)

print("第二次調用執行結果(傳入參數):")
print(g.send(7))
print("*" * 100)

print("第三次調用執行結果:")
print(next(g))
print("*" * 100)

 

第一次調用執行結果:
Starting.....
4
*****************************************************************
第二次調用執行結果(傳入參數):
res: 7
4
******************************************************************
第三次調用執行結果:
res: None
4
******************************************************************

 

  • send 函數的概念:003 案例中第二次調用時 res 的值為什么是 None,這個變成了 7,到底為什么?
  • 這是因為,send 是發送一個參數給 res 的,因為上面講到,return 的時候,並沒有把 4 賦值給 res,下次執行的時候只好繼續執行賦值操作,只好賦值為 None 了,而如果用 send 的話,開始執行的時候,先接着上一次(return 4 之后)執行,先把 7 賦值給了 res,然后執行 next 的作用,遇見下一回的 yield,return 出結果后結束(return 的結果都是 4,每次代碼最后的結果都是 4)。
  • 上面代碼執行步驟:
    1. 程序執行 g.send(7),程序會從 yield 關鍵字那一行繼續向下運行,send 會把 7 這個值賦值給 res 變量
    2. 由於 send 方法中包含 next()方法,所以程序會繼續向下運行執行 print 方法,然后再次進入 while 循環
    3. 程序執行再次遇到 yield 關鍵字,yield 會返回后面的值后,程序再次暫停,直到再次調用 next 方法或 send 方法。
深層次補充:(上面的案例描述只是為了容易理解,描述為暫停和賦值)
  • 比如說“send 方法中包含 next()”send 先賦值然后在執行 next,從一些代碼直觀上來講好像是這樣,但其實並不是,
  • 第一,其實並不是賦值,
  • 第二,底層 send 和 next 其實都是調用 gensendex(PyGenObject *gen,PyObject *arg,int exc)這個函數,只是第二個參數不一樣,send 也不一定要帶參數,尤其是第一次使用 send 來啟動生成器,send 帶參數還是不允許的。
  • 如果對中斷了解的話,其實不要把這個當成 return 來看,因為根本就不是,應該當成中斷來理解,
  • 因為底層的實現就是中斷的原理,保存棧幀,加載棧幀。

4. yield 的好處是什么?

通過上面的閱讀和敲代碼已經理解了什么是 yield,和整個執行原理都應該很清楚了,單究竟為什么要使用 yield,而不是用 return???

我們以列表 list 為例,為什么用這個生成器,是因為如果用 List 的話,會占用更大的空間, 比如說取 0,1,2,3,4,5,6............1000,下面舉例,只取到 10,1000 結果太長了

for n in range(10):
    a=n
    print(a) # 相當於 return a
print("*" * 100)

 

0
1
2
3
4
5
6
7
8
9

 

生成器實現上面功能

# 生成器實現
def foo(num):
    print("starting...")
    while num<10:
        num=num+1
        yield num

for n in foo(0):
    print(n)

 

starting...
1
2
3
4
5
6
7
8
9
10

 

  • 上面兩種方式都可以得到 0-10 之間的數字,但是占用內存不同:

  • 第一種直接使用 for 循環: for 循環運行時,所有的 0-10 之間數字都存在內存之中需要消耗極大的內存,如果數字是 10000,可能 for 循環直接就將電腦內存消耗完了后面的代碼,其它程序就無內存可用了

  • 第二種,雖然也是 for 循環,但是內部加入了 yield:for 循環每次調用時,yield 生成器(generator)能夠迭代的關鍵是它有一個 next()方法,工作原理就是通過重復調用 next()方法,直到捕獲一個異常,for 循環自動結束

  • 每次執行到 yield,因為底層的實現就是中斷的原理,保存棧幀,加載棧幀。 每次執行結束內存釋放,執行的時候占用一點內存,消耗的內存資源就很少

  • 上面 for 循環執行過程,並沒有寫 next 函數,其實自動調用的 next 函數(參考迭代器與迭代對象中詳細解釋):

  • for 循環執行過程:

  1. 調用可迭代對象的iter方法返回一個迭代器對象(iterator)
  2. 不斷調用迭代器的next方法返回元素
  3. 直到迭代完成后,處理 StopIteration 異常
  • 在這里插入圖片描述

  •  

yield 的好處總結:

  1. 不會將所有數據取出來存入內存中;而是返回了一個對象;可以通過對象獲取數據;用多少取多少,可以節省內容空間。
  2. 除了能返回一個值,還不會終止循環的運行;
  3. 每次執行到 yield,因為底層的實現就是中斷的原理,保存棧幀,加載棧幀。
  4. 每次執行結束內存釋放,執行的時候占用一點內存,消耗的內存資源就很少

補充:

  • 通常 yield 都是放在一個函數中,該函數就變成了生成器函數,該函數就變成了一個迭代器
  • 生成器函數一般都是通過 for 循環調用,for 循環自帶 next 方法
  • 分布式爬蟲會經常使用 yield,yield 直接放在 for 循環的內部
  • 爬蟲代碼運行時候,for 循環自動調用 next 方法,yield 就會不斷執行,直到爬取結束
  • 使用 yield 也會大大減少爬蟲運行時候的內存消耗


免責聲明!

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



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