Python 中的 Iterator 是什麼

iterator 的好處以及它與 generator 或 yield 關鍵字的關係

Jim

Jim

2023年4月4日 上午 12:58

技術文章

在 Python 中,iterator (迭代器)是一種特殊的物件,它可以逐步遍歷序列中的每一個元素。與 list 不同,iterator 不會在記憶體中保存整個序列,而是在需要時逐步計算出下一個元素的值,從而節省記憶體空間。這篇文章會說明什麼是 iterator,iterator 的好處以及它與 generator 或 yield 關鍵字的關係。

什麼是 Iterator

我們在 Python 中經常使用 for 迴圈來處理序列、進行迭代操作,Python 中的 iterator 物件就是用來支援這種迭代操作的,它可以讓我們逐一取出序列中的元素。我們可以使用 iter() 函數來建立一個 iterator 物件,並使用 next() 方法來取得下一個元素。如以下範例:

1
2
3
4
5
6
my_list = [1, 2, 3, 4, 5]
my_iterator = iter(my_list)
 
print(next(my_iterator)) # 1
print(next(my_iterator)) # 2
print(next(my_iterator)) # 3

在這個範例中,我們先建立一個包含五個元素的 list,接著使用 iter() 函式建立一個 iterator 物件 my_iterator,然後使用 next() 方法依序取得它的元素。範例程式碼中的 next() 方法依序取得了 my_iterator 的前三個元素(也就是 1、2、3)。

Iterator 的好處

使用 iterator 的優點在於節省記憶體。當你使用 iterator 處理大量資料時,它只會在需要時才生成資料,而不是一次生成整個序列。舉例來說,假設你需要建立一個包含一百萬個元素的 list,這一百萬個元素會一次被產生出來,並佔用記憶體空間。但是,如果你使用 iterator,你可以透過 generator expression 依序生成這些元素,而不需要在記憶體中一次儲存整個 list。如以下範例:

1
2
3
4
5
6
7
8
9
10
11
import sys
 
my_list = [x**2 for x in range(1000000)]
my_generator = (x**2 for x in range(1000000))
 
print(f"Size of my_list: {sys.getsizeof(my_list)} bytes. Type: {type(my_list)}")
print(f"Size of my_generator: {sys.getsizeof(my_generator)} bytes. Type: {type(my_generator)}")
 
# 執行結果
# Size of my_list: 8697456 bytes. Type: <class 'list'>
# Size of my_generator: 112 bytes. Type: <class 'generator'>

你可以看到這兩個變數佔用的記憶體空間差距有多大(8 MB 與 112 bytes),當資料量很龐大時,使用 iterator 的好處會更明顯。

「只在需要時產生元素」的特性,也可以理解為 Lazy Loading (或 Lazy Evaluation). 因為一次只處理序列中的一個元素,iterator 的高效率在需要大量計算才能產生序列中的元素時特別明顯。如以下範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import time
 
# 模擬真實世界的緩慢操作,如檔案下載或大量計算
def slow_operation(n):
    time.sleep(n)
    return n
 
# 把所有元素處理完畢後,放在 list 回傳
def get_with_list(elements):
    results = []
    for e in elements:
        results.append(slow_operation(e))
    return results
 
# 使用 yield 把以下 function 變成 generator
def get_with_generator(elements):
    for e in elements:
        yield slow_operation(e)
 
numbers = [1, 1, 5, 1, 10]
 
# 找出 numbers 序列中第一個經過處理後大於 4 的數字
print("get_with_list():")
for n in get_with_list(numbers):   
    if n > 4:
        print(f"The number greater than 4 after processed is {n}")
        break
 
print("get_with_generator():")
for n in get_with_generator(numbers):   
    if n > 4:
        print(f"The number greater than 4 after processed is {n}")
        break
 
# 執行結果
# get_with_list():
# The number greater than 4 after processed is 5
# get_with_generator():
# The number greater than 4 after processed is 5

get_with_list() 會把整個序列處理完且產生之後才回傳結果,所以 for n in get_with_list(numbers) 這個迴圈大約需要 1+1+5+1+10 秒;而 get_with_generator() 會依序處理 elements 裡面的元素,且一次回傳一個結果,所以 for n in get_with_generator(numbers) 這個迴圈只需要 1+1+5 秒(numbers 內最後兩個數字被處理到之前,迴圈已結束)。

for 迴圈也可以遍歷 list,那 list 是 iterator 嗎?你搞得我好亂啊

用 for 迴圈遍歷一個 list 與一個 iterator 的結果是完全相同的:

1
2
3
4
5
6
7
8
9
my_list = [1, 2, 3, 4, 5]
my_iterator = iter(my_list)
 
# 以下兩段程式輸出結果完全相同
for n in my_list: 
    print(n)
 
for n in my_iterator
    print(n)

但是,list 不是 iterator, 只是個 iterable (可迭代) 類別。在 Python 中,任何可迭代物件都可以被 for 迴圈使用,而不僅僅限於 iterator.

當我們使用 for 迴圈迭代 iterable 物件時,Python 會在幕後自動呼叫物件的 iter() 方法取得一個 iterator 物件,再利用該物件依次取得序列中的每個數值。所以在以上例子中,Python 會自動呼叫my_listiter() 方法,取得一個 iterator 物件,然後再呼叫該物件的 next() 方法,逐個取得序列中的每個數值。遍歷 my_listmy_iterator 的輸出結果相同,但這兩個變數佔用的記憶體空間不同,一開始的例子已經說明過了。

Generator 跟 yield 是什麼?它們跟 iterator 有什麼關係?

Generator 是 iterator 的一種實現方式。generator 是一種使用函式來實現的 iterator,它可以動態生成序列,並且每次只生成一個值。generator 的運作方式類似於函式,它可以接受一些參數,然後根據這些參數生成一個序列。

Generator 的語法非常簡單,只需要在函式中使用 yield 關鍵字(而不是 return)回傳結果即可,例如以下範例:

1
2
3
4
5
6
7
8
def countdown(n):
    while n > 0:
        yield n
        n -= 1
 
# 從 10 倒數到 1
for i in countdown(10):
    print(i)

yield 取代 return 的差異在於:它讓 countdown() 函數可以記住上一次迭代時的狀態。當 countdown() 第一次被呼叫時,它會停在 yield n 這一行,並回傳 10;第二次被呼叫時,它會從上一次停止的地方繼續執行(也就是 yield n 的下一行,n -= 1),直到再次遇到 yield,才暫停並回傳 9……以此類推。換句話說,countdown() 函數每次只生成一個值,而不是一次性在記憶體中產生整個序列。

如何自定義一個 Iterator 物件

只需要定義__iter__()__next__() 這兩個必要方法即可。其中,__iter__() 方法返回迭代器物件本身,而__next__() 方法則返回下一個值。當序列中沒有值時,__next__()方法應該拋出StopIteration 異常。

以下是一個簡單的迭代器例子,它能夠生成指定範圍內的奇數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class OddIterator:
    def __init__(self, limit):
        self.limit = limit
        self.current = 1
 
    def __iter__(self):
        return self
 
    def __next__(self):
        if self.current <= self.limit:
            result = self.current
            self.current += 2
            return result
        else:
            raise StopIteration
 
# 使用方式: 印出 1, 3, 5, 7, 9
my_iterator = OddIterator(10)
for num in my_iterator:
    print(num)

想系統化學習更多 Python 資料型態與進階實務觀念,可以參考 Python 練功坊: 50 道精選練習題助你掌握 Python 實務觀念

文章標籤

# python