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
importsys
my_list =[x**2forx inrange(1000000)]
my_generator =(x**2forx inrange(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
importtime
# 模擬真實世界的緩慢操作,如檔案下載或大量計算
defslow_operation(n):
time.sleep(n)
returnn
# 把所有元素處理完畢後,放在 list 回傳
defget_with_list(elements):
results =[]
fore inelements:
results.append(slow_operation(e))
returnresults
# 使用 yield 把以下 function 變成 generator
defget_with_generator(elements):
fore inelements:
yieldslow_operation(e)
numbers =[1, 1, 5, 1, 10]
# 找出 numbers 序列中第一個經過處理後大於 4 的數字
print("get_with_list():")
forn inget_with_list(numbers):   
ifn > 4:
print(f"The number greater than 4 after processed is {n}")
break
print("get_with_generator():")
forn inget_with_generator(numbers):   
ifn > 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)
# 以下兩段程式輸出結果完全相同
forn inmy_list: 
print(n)
forn inmy_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
defcountdown(n):
whilen > 0:
yieldn
n -=1
# 從 10 倒數到 1
fori incountdown(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
classOddIterator:
def__init__(self, limit):
self.limit =limit
self.current =1
def__iter__(self):
returnself
def__next__(self):
ifself.current <=self.limit:
result =self.current
self.current +=2
returnresult
else:
raiseStopIteration
# 使用方式: 印出 1, 3, 5, 7, 9
my_iterator =OddIterator(10)
fornum inmy_iterator:
print(num)

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

文章標籤

# python