轉自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 ********************************************************************
- 下面解釋代碼運行順序,相當於代碼單步調試():
- 程序開始執行以后,因為 foo 函數中有 yield 關鍵字,所以 foo 函數並不會真的執行, 而是先得到一個生成器 g(相當於一個對象),函數的一個狀態,函數相當於暫停了
- 執行第一次調用,直到遇到 next 方法,foo 函數正式開始執行,先執行 foo 函數中的 print 方法,然后進入 while 循環
- 程序遇到 yield 關鍵字,然后把 yield 想想成 return,return 了一個 4 之后,程序停止, 但是,程序只是返回了一個值 4,並沒有執行將 4 賦值給 res 操作,此時 next(g)語句執行完成, 所以第一次調用后的結果有兩行(第一個是 while 上面的 print 的結果,第二個是 return 出的結果) 也就是執行 print(next(g))先調用函數,最后打印出了返回值 4
- 程序執行 print("*" * 100),輸出 100 個*
- 執行第二次調用,又開始執行下面的 print(next(g)),這個時候和上面那個差不多,不過不同的是,這個時候是從剛才那個 next 程序停止的地方開始執行的,也就是要執行 res 的賦值操作語句, 這時候要注意,yield 4 返回值 4 后就停止了,並沒有賦值給前面的 res, (因為剛才那個是 return 出去了,並沒有給賦值操作的左邊傳參數),此時代碼實際是從 print("res:", res)開始執行, 這個時候 res 賦值是空,是 None,所以接着下面的輸出就是 res:None,
- 程序會繼續在 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)。
- 上面代碼執行步驟:
- 程序執行 g.send(7),程序會從 yield 關鍵字那一行繼續向下運行,send 會把 7 這個值賦值給 res 變量
- 由於 send 方法中包含 next()方法,所以程序會繼續向下運行執行 print 方法,然后再次進入 while 循環
- 程序執行再次遇到 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 循環執行過程:
- 調用可迭代對象的iter方法返回一個迭代器對象(iterator)
- 不斷調用迭代器的next方法返回元素
- 直到迭代完成后,處理 StopIteration 異常
yield 的好處總結:
- 不會將所有數據取出來存入內存中;而是返回了一個對象;可以通過對象獲取數據;用多少取多少,可以節省內容空間。
- 除了能返回一個值,還不會終止循環的運行;
- 每次執行到 yield,因為底層的實現就是中斷的原理,保存棧幀,加載棧幀。
- 每次執行結束內存釋放,執行的時候占用一點內存,消耗的內存資源就很少
補充:
- 通常 yield 都是放在一個函數中,該函數就變成了生成器函數,該函數就變成了一個迭代器
- 生成器函數一般都是通過 for 循環調用,for 循環自帶 next 方法
- 分布式爬蟲會經常使用 yield,yield 直接放在 for 循環的內部
- 爬蟲代碼運行時候,for 循環自動調用 next 方法,yield 就會不斷執行,直到爬取結束
- 使用 yield 也會大大減少爬蟲運行時候的內存消耗

