Python 程式碼中的 yield 到底是什麼?

WanWuJieKeLian發表於2024-07-28

在Python程式設計中,有一個強大而神秘的關鍵字,那就是yield。初學者常常被它搞得暈頭轉向,而高階開發者則藉助它實現高效的程式碼。到底yield是什麼?它又是如何在Python程式碼中發揮作用的呢?讓我們一起來揭開它的面紗。

Python裡的一個非常重要但也頗具迷惑性的關鍵詞——yield

什麼是yield?為什麼我們需要在Python中使用它?

來,讓我們一起來拆解一下,看看yield到底是個啥。

迭代與可迭代物件


要搞明白yield,咱們先得弄清楚什麼是可迭代物件(iterables)。

所謂可迭代物件,簡單來說,就是你可以逐個讀取其元素的物件,比如列表、字串、檔案等等。舉個例子,當你建立一個列表時,你可以用for迴圈一個個地讀取它的元素:

mylist = [1, 2, 3]
for i in mylist:
print(i)

輸出會是:

1
2
3

這裡的mylist就是一個可迭代物件。你還可以用列表推導式(list comprehension)來建立一個列表,它同樣也是可迭代的:​​​​​​​

mylist = [x*x for x in range(3)]
for i in mylist:
print(i)

輸出是:​​​​​​​

0
1
4

凡是你可以用for... in...來操作的東西,都是可迭代物件,包括列表、字串、檔案等等。

可迭代物件非常方便,因為你可以任意多次地讀取它們的值,但前提是你得把所有值都儲存在記憶體裡。這就帶來了一個問題:當資料量很大時,這種方式顯然不太合適。

生成器


生成器(generators)是迭代器的一種,你只能遍歷它們一次。生成器不像列表那樣把所有的值都儲存在記憶體裡,而是即用即生成。來看看生成器的例子:​​​​​​​

mygenerator = (x*x for x in range(3))
for i in mygenerator:
print(i)

輸出和列表推導式一樣:​​​​​​​

0
1
4

但注意了,生成器只能使用一次,因為它們會“邊用邊忘”:計算0後忘記0,計算1後忘記1,最後計算4後結束。再用同一個生成器物件做for迴圈就沒有結果了。

yield關鍵詞


說到yield,這是個類似於return的關鍵詞,但它返回的不是一個值,而是一個生成器。看看這個例子:​​​​​​​

def create_generator():
mylist = range(3)
for i in mylist:
yield i*i

mygenerator = create_generator() # 建立一個生成器
print(mygenerator) # mygenerator 是一個生成器物件!

輸出是:

<generator object create_generator at 0xb7555c34>

透過for迴圈遍歷這個生成器:​​​​​​​

for i in mygenerator:
print(i)

輸出:​​​​​​​

0
1
4

這個例子看起來簡單,但它在處理大量資料時特別有用,因為生成器只在需要時生成值,而不是一次性生成所有值然後儲存在記憶體中。

深入理解yield


為了徹底掌握yield,我們需要理解當呼叫生成器函式時,函式體內的程式碼並不會立即執行。函式返回的是一個生成器物件,然後你的程式碼會在每次呼叫for迴圈時從上次中斷的地方繼續執行,直到遇到下一個yield。

第一次呼叫for迴圈時,生成器物件會從頭開始執行函式中的程式碼,直到遇到yield,然後返回迴圈中的第一個值。隨後的每次呼叫都會執行函式中迴圈的下一次迭代,直到生成器不再有值返回。這可能是因為迴圈結束了,或者條件不再滿足。

來看看一個實際的例子:​

1 def _get_child_candidates(self, distance, min_dist, max_dist):
2     if self._leftchild and distance - max_dist < self._median:
3         yield self._leftchild
4     if self._rightchild and distance + max_dist >= self._median:
5         yield self._rightchild

這裡的程式碼在每次使用生成器物件時都會被呼叫:

如果節點物件還有左子節點並且距離合適,返回下一個子節點。

如果節點物件還有右子節點並且距離合適,返回下一個子節點。

如果沒有更多子節點,生成器會被認為是空的。

呼叫這個生成器的方法如下:​​​​​​​

1 result, candidates = list(), [self]
2 while candidates:
3     node = candidates.pop()
4     distance = node._get_dist(obj)
5     if distance <= max_dist and distance >= min_dist:
6         result.extend(node._values)
7     candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
8 
9 return result

這裡的程式碼有幾個巧妙之處:

  • 迴圈遍歷一個列表,而列表在迴圈過程中會擴充套件。這樣可以方便地遍歷所有巢狀的資料,雖然有些危險,因為可能會陷入無限迴圈。在這個例子中,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))用盡生成器的所有值,但while迴圈不斷建立新的生成器物件,因為它們作用在不同的節點上會產生不同的值。
  • extend()方法是列表物件的方法,它期望一個可迭代物件,並將其值新增到列表中。通常我們傳遞一個列表給它,但在程式碼中,它接收一個生成器,這是個好主意,因為:
  • 你不需要讀取值兩次。
  • 你可能有很多子節點,不想全部儲存在記憶體中。

這段程式碼展示了Python為何如此酷:它不在乎方法的引數是列表還是其他可迭代物件。這種特性叫鴨子型別(duck typing),也是Python靈活性的一個體現。

高階用法


再來看一個更高階的用法——控制生成器的耗盡:​​​​​​​

 1 class Bank():
 2     crisis = False
 3     def create_atm(self):
 4         while not self.crisis:
 5             yield "$100"
 6 
 7 hsbc = Bank()
 8 corner_street_atm = hsbc.create_atm()
 9 print(next(corner_street_atm)) # 輸出 $100
10 print(next(corner_street_atm)) # 輸出 $100
11 print([next(corner_street_atm) for _ in range(5)]) # 輸出 ['$100', '$100', '$100', '$100', '$100']
12 
13 hsbc.crisis = True
14 print(next(corner_street_atm)) # 輸出 StopIteration

這裡我們模擬了一個ATM機,在銀行沒有危機時,你可以不斷取錢,但一旦危機來了,ATM機就會停止工作,即使是新的ATM機也不能再取錢了。

itertools模組


最後,給大家介紹一個非常有用的模組——itertools。這個模組包含了很多操作可迭代物件的特殊函式。如果你曾經希望複製一個生成器、連線兩個生成器、用一行程式碼將值分組到巢狀列表中,或者在不建立另一個列表的情況下使用map和zip,那麼就應該匯入itertools。

舉個例子,我們看看四匹馬比賽的可能到達順序:​​​​​​​

import itertools

horses = [1, 2, 3, 4]
races = itertools.permutations(horses)
print(list(itertools.permutations(horses)))

輸出:

[(1, 2, 3, 4), (1, 2, 4, 3), (1, 3, 2, 4), (1, 3, 4, 2), (1, 4, 2, 3), (1, 4, 3, 2), (2, 1, 3, 4), (2, 1, 4, 3), (2, 3, 1, 4), (2, 3, 4, 1), (2, 4, 1, 3), (2, 4, 3, 1), (3, 1, 2, 4), (3, 1, 4, 2), (3, 2, 1, 4), (3, 2, 4, 1), (3, 4, 1, 2), (3, 4, 2, 1), (4, 1, 2, 3), (4, 1, 3, 2), (4, 2, 1, 3), (4, 2, 3, 1), (4, 3, 1, 2), (4, 3, 2, 1)]

itertools模組簡直是Python程式設計師的好夥伴,可以讓你在處理迭代物件時如虎添翼。

總結


yield是Python中一個強大的工具,它可以幫助你以一種高效的方式處理大量資料。理解yield的工作原理對於掌握Python程式設計至關重要。

在大資料時代,處理海量資料已成為常態。生成器作為一種高效的資料處理方式,因其優越的記憶體管理能力,受到了越來越多開發者的青睞。無論是日誌處理、資料流分析,還是實時資料處理,生成器都展現了不可替代的價值。

透過對yield的詳解,我們不僅理解了它的基本概念和用法,還認識到它在高效資料處理中的重要性。掌握yield,將為你的Python程式設計之旅增添一把利器。

相關文章