Python 迭代器與生成器的具體使用
本文面向已有前端開發(fā)基礎、正在學習 Python 的開發(fā)者。
迭代器和生成器解決的是同一個問題:數(shù)據(jù)不一定要一次性全部準備好,可以在需要的時候一個一個取出來。前端里最接近的經驗是 for...of、Symbol.iterator、生成器函數(shù) function* 和 yield。
這幾個概念可以先合在一起記:
- 可迭代對象表示“可以被遍歷的數(shù)據(jù)源”
- 迭代器表示“真正負責一步一步取值的對象”
- 生成器表示“用
yield快速創(chuàng)建出來的迭代器”
后面的for循環(huán),本質上就是先從可迭代對象拿到迭代器,再不斷從迭代器里取下一個值。
一、先把概念邊界講清楚
先記住一條主線:
for item in obj -> 先調用 iter(obj) 拿到迭代器 -> 再不斷調用 next(迭代器) -> 遇到 StopIteration 后結束
所以這幾個概念可以這樣分:
| 概念 | 關注點 | Python | JavaScript |
|---|---|---|---|
| 可迭代對象 | 能不能開始遍歷 | 能被 iter() 接受 | 有 Symbol.iterator |
| 迭代器 | 這次遍歷走到哪里了 | 能被 next() 調用 | 有 next() |
| 迭代協(xié)議 | 遍歷接口怎么約定 | iter() -> next() -> 結束時拋異常 | Symbol.iterator -> next() |
| 生成器 | 怎么快速創(chuàng)建迭代器 | 函數(shù)體里寫 yield | function* + yield |
兩門語言只是接口名字和結束方式不同:
Python:
可迭代對象 -- iter() --> 迭代器 -- next() --> value / StopIteration
JS:
可迭代對象 -- Symbol.iterator() --> 迭代器 -- next() --> { value, done }最關鍵的邊界是:可迭代對象表示“能開始一次遍歷”,迭代器表示“這次遍歷本身”。列表、字符串這類可迭代對象可以反復遍歷,因為每次都能創(chuàng)建新的迭代器;已經創(chuàng)建出來的迭代器通常只能向前走,取過的值不會自動回到起點。
生成器不算新的遍歷體系,它只是更省事的迭代器寫法。手寫迭代器要自己維護位置和結束條件;生成器用 yield 保存暫停點,每次 next() 都從上一次暫停的位置繼續(xù)執(zhí)行。
是不是有點懵??
二、從 for 循環(huán)看迭代過程
JavaScript 里,一個對象只要實現(xiàn)了 Symbol.iterator,就可以被 for...of 消費。
const names = ["張三", "李四", "王五"];
for (const name of names) {
console.log(name);
}
如果拆開看,for...of 背后大概做了這些事:
const iterator = names[Symbol.iterator]();
console.log(iterator.next()); // { value: '張三', done: false }
console.log(iterator.next()); // { value: '李四', done: false }
console.log(iterator.next()); // { value: '王五', done: false }
console.log(iterator.next()); // { value: undefined, done: true }
所以前端里有兩層概念:
| 概念 | 判斷方式 | 作用 |
|---|---|---|
| iterable | 有 Symbol.iterator | 可以交給 for...of |
| iterator | 有 next() | 可以一步一步取值 |
Python 也有這兩層,只是名字和結束方式不同:
| JavaScript | Python |
|---|---|
| obj[Symbol.iterator]() | iter(obj) |
| iterator.next() | next(iterator) |
| 返回 { value, done } | 返回本次值 |
| done: true 表示結束 | 拋出 StopIteration 表示結束 |
把這個對照關系記住,后面的 Python 語法就會清楚很多。
三、可迭代對象 iterable
可迭代對象就是:能被 for 循環(huán)遍歷的對象。
names = ['張三', '李四', '王五']
cities = ('北京', '上海', '深圳')
msg = 'hello'
for name in names:
print(name)
這些對象都能被 for 遍歷,所以它們都是可迭代對象。
從協(xié)議角度看,可迭代對象要能被 iter() 接受:
names = ['張三', '李四', '王五'] msg = 'hello' age = 18 print(iter(names)) # list_iterator print(iter(msg)) # str_iterator # print(iter(age)) # TypeError: 'int' object is not iterable
也可以用 hasattr 粗略觀察:
names = ['張三', '李四', '王五'] msg = 'hello' age = 18 print(hasattr(names, '__iter__')) # True print(hasattr(msg, '__iter__')) # True print(hasattr(age, '__iter__')) # False
這里的 __iter__ 是 Python 的魔法方法。平時開發(fā)一般不直接寫 names.__iter__(),而是用內置函數(shù) iter(names)。
obj.__iter__() -> 底層魔法方法 iter(obj) -> 日常使用方式 -> 內部會調用 obj.__iter__()
四、迭代器 iterator
調用 iter(可迭代對象) 之后,會得到一個迭代器。
names = ['張三', '李四', '王五'] it = iter(names) print(next(it)) # 張三 print(next(it)) # 李四 print(next(it)) # 王五 print(next(it)) # StopIteration
迭代器的核心能力是:記住當前取到哪里了,每次 next() 返回下一個值。
也就是說,迭代器內部有狀態(tài),類似一個指針:
初始位置
-> next() 取第 1 個
-> next() 取第 2 個
-> next() 取第 3 個
-> 沒有數(shù)據(jù)了,拋 StopIteration
如果用 while 手動模擬 for,大概是這樣:
names = ['張三', '李四', '王五']
it = iter(names)
while True:
try:
item = next(it)
print(item)
except StopIteration:
break
所以 for item in names 并不神秘,它背后就是:
先調用 iter(names) 得到迭代器
再不斷調用 next(迭代器)
遇到 StopIteration 后結束循環(huán)
迭代器自己也是可迭代對象
迭代器一般也有 __iter__ 方法,并且返回自己。
names = ['張三', '李四', '王五'] it = iter(names) print(iter(it) is it) # True
這樣設計的原因是:for 循環(huán)第一步一定會調用 iter(x)。如果傳進去的已經是迭代器,iter(迭代器) 必須也能正常工作。
迭代器會被消耗
迭代器不是列表,它是一次性向前取值的過程。
names = ['張三', '李四', '王五']
it = iter(names)
print(next(it)) # 張三
for name in it:
print(name)
# 只會繼續(xù)輸出:
# 李四
# 王五
前面已經被 next(it) 取走的值,不會在后面的 for 里重新出現(xiàn)。
這點很像前端里已經調用過幾次 iterator.next() 后,再繼續(xù) for...of 或繼續(xù) .next(),狀態(tài)會接著往后走,而不是自動重置。
五、自定義可迭代對象
如果希望自己的類能被 for 遍歷,就要實現(xiàn)迭代器協(xié)議。
需求:讓 Person 實例可以被遍歷,依次取出姓名、年齡、性別、地址。
p1 = Person('張三', 18, '男', '北京昌平')
for item in p1:
print(item)
寫法一:對象和迭代器分開
這種寫法最清晰:Person 負責保存業(yè)務數(shù)據(jù),PersonIterator 負責遍歷過程。
class Person:
def __init__(self, name, age, gender, address):
self.name = name
self.age = age
self.gender = gender
self.address = address
def __iter__(self):
# 返回一個專門負責遍歷 Person 的迭代器
return PersonIterator(self)
class PersonIterator:
def __init__(self, person):
# 保存外部傳進來的 Person 對象
self.person = person
# 記錄當前取到哪個位置
self.index = 0
# 配置要遍歷哪些字段
self.attrs = [
person.name,
person.age,
person.gender,
person.address,
]
def __iter__(self):
# 迭代器的 __iter__ 返回自己
return self
def __next__(self):
if self.index >= len(self.attrs):
raise StopIteration
value = self.attrs[self.index]
self.index += 1
return value
執(zhí)行:
p1 = Person('張三', 18, '男', '北京昌平')
for item in p1:
print(item)
輸出:
張三
18
男
北京昌平
這個寫法適合業(yè)務對象比較復雜的場景。業(yè)務對象和遍歷狀態(tài)分開,Person 不需要關心當前遍歷到第幾個字段。
寫法二:對象自己也是迭代器
也可以讓 Person 同時實現(xiàn) __iter__ 和 __next__。
class Person:
def __init__(self, name, age, gender, address):
self.name = name
self.age = age
self.gender = gender
self.address = address
self.attrs = [name, age, gender, address]
def __iter__(self):
self.index = 0
return self
def __next__(self):
if self.index >= len(self.attrs):
raise StopIteration
value = self.attrs[self.index]
self.index += 1
return value
這種寫法代碼更少,但要注意:遍歷狀態(tài)放在對象自己身上。多個地方同時遍歷同一個對象時,更容易相互影響。
學習階段可以先寫這種,真實業(yè)務里更推薦“對象和迭代器分開”,職責更清楚。
六、為什么需要迭代器
迭代器最大的價值是惰性計算:不一次性生成所有結果,而是在需要時才計算下一個。
比如生成斐波那契數(shù)列,如果一次性生成 100000 個數(shù)字并放進列表,內存會越來越大。
def fib_list(total):
result = []
a = 0
b = 1
for _ in range(total):
result.append(a)
a, b = b, a + b
return result
如果改成迭代器,每次只返回當前這個數(shù):
class Fibo:
def __init__(self, total):
self.total = total
self.index = 0
self.a = 0
self.b = 1
def __iter__(self):
return self
def __next__(self):
if self.index >= self.total:
raise StopIteration
value = self.a
self.a, self.b = self.b, self.a + self.b
self.index += 1
return value
使用:
for number in Fibo(10):
print(number)
迭代器適合這些場景:
- 數(shù)據(jù)量很大,不想一次性放進內存
- 不確定用戶最終會消費多少結果
- 數(shù)據(jù)來自文件、網(wǎng)絡、數(shù)據(jù)庫游標這類流式來源
- 每個結果只依賴當前狀態(tài)和上一個狀態(tài)
七、生成器 generator
生成器可以理解成:Python 幫你自動實現(xiàn)迭代器協(xié)議的語法糖。
只要一個函數(shù)體里出現(xiàn) yield,這個函數(shù)就不是普通函數(shù),而是生成器函數(shù)。
def demo():
print('demo 函數(shù)開始執(zhí)行了')
print(100)
yield '我是第 1 個 yield 返回的數(shù)據(jù)'
a = 200
print(a)
yield '我是第 2 個 yield 返回的數(shù)據(jù)'
b = 300
print(b)
return '執(zhí)行結束'
調用生成器函數(shù)時,函數(shù)體不會立刻執(zhí)行,而是返回一個生成器對象。
d = demo() print(hasattr(d, '__iter__')) # True print(hasattr(d, '__next__')) # True
生成器對象本質上是一種迭代器,所以可以用 next() 取值:
d = demo()
print(next(d))
print(next(d))
try:
print(next(d))
except StopIteration as e:
print(e.value) # 執(zhí)行結束
執(zhí)行過程可以這樣理解:
第一次 next()
-> 函數(shù)從開頭執(zhí)行
-> 遇到第一個 yield 暫停
-> yield 后面的值作為本次 next() 的返回值第二次 next()
-> 從上次暫停的位置繼續(xù)執(zhí)行
-> 遇到第二個 yield 再暫停第三次 next()
-> 繼續(xù)執(zhí)行
-> 遇到 return
-> 拋 StopIteration
-> return 后面的值會放到異常對象的 value 里
生成器和普通函數(shù)最大的差異是:普通函數(shù)一次調用跑到底,生成器函數(shù)可以在 yield 處暫停,下次再接著跑。
前端里可以對照 function*:
function* demo() {
console.log("demo 開始執(zhí)行");
yield "第 1 個值";
yield "第 2 個值";
}
const d = demo();
console.log(d.next());
console.log(d.next());
console.log(d.next());
八、yield 的幾個常見寫法
yield 寫在循環(huán)里
最常見的生成器寫法,是在循環(huán)里不斷 yield。
def fib(total):
a = 0
b = 1
for _ in range(total):
yield a
a, b = b, a + b
使用:
for number in fib(10):
print(number)
這比手寫 class Fibo 簡潔很多,但效果類似:每次需要下一個值時,才繼續(xù)往后計算。
yield from
yield from 可以把另一個可迭代對象里的值依次產出。
def demo():
nums = [10, 20, 30, 40]
yield from nums
它大致等價于:
def demo():
nums = [10, 20, 30, 40]
for num in nums:
yield num
所以 yield from 可以記成:
把某個可迭代對象里的數(shù)據(jù),一個一個 yield 出去
send()
生成器除了能往外吐值,也能在繼續(xù)執(zhí)行時接收外部傳進來的值。
def demo():
print('demo 函數(shù)開始執(zhí)行了')
a = yield '第 1 個 yield 的返回值'
print(f'a 接收到:{a}')
b = yield '第 2 個 yield 的返回值'
print(f'b 接收到:')
使用:
d = demo()
print(next(d)) # 先啟動生成器,停在第一個 yield
print(d.send('張三')) # 把 '張三' 傳給變量 a,然后繼續(xù)執(zhí)行
try:
d.send('李四') # 把 '李四' 傳給變量 b,然后繼續(xù)執(zhí)行到函數(shù)結束
except StopIteration:
print('生成器執(zhí)行結束')
注意:第一次啟動生成器時不能直接傳普通值,因為代碼還沒有運行到任何一個 yield 位置,沒有地方接收這個值。
d = demo()
# d.send('張三') # TypeError
d.send(None) # 等價于 next(d)
next() 只能取值;send(value) 既能讓生成器繼續(xù)執(zhí)行,也能把值傳回上一次暫停的 yield 表達式。
九、生成器表達式
生成器表達式是一種快速創(chuàng)建生成器對象的寫法,長得很像列表推導式。
nums = [10, 20, 30, 40] result1 = [n * 2 for n in nums] result2 = (n * 2 for n in nums) print(result1) # [20, 40, 60, 80] print(result2) # <generator object ...>
區(qū)別在于:
| 寫法 | 結果 | 是否立刻生成全部結果 |
|---|---|---|
| [n * 2 for n in nums] | 列表 | 是 |
| (n * 2 for n in nums) | 生成器對象 | 否 |
生成器表達式適合“每個結果只依賴當前元素”的場景。
nums = [10, 20, 30, 40]
result = (n * 2 for n in nums)
for item in result:
print(item)
它不會一次性創(chuàng)建 [20, 40, 60, 80],而是每次循環(huán)時才計算當前這個 item。
如果數(shù)據(jù)量很小,并且后面要反復使用結果,列表推導式更直觀。如果數(shù)據(jù)量很大,只需要順序消費一遍,生成器表達式更省內存。
十、最后怎么選
可以按這個順序判斷:
只是遍歷已有 list / tuple / dict / str
-> 直接 for想讓自己的類能被 for
-> 實現(xiàn) __iter__
-> 如果要自己控制取值過程,再實現(xiàn) __next__要一個一個惰性產出結果
-> 優(yōu)先寫生成器函數(shù) yield只是把一個可迭代對象映射成另一個惰性結果
-> 用生成器表達式需要復雜狀態(tài)、多個方法、可維護的對象封裝
-> 手寫迭代器類
最容易混淆的點:
| 問題 | 結論 |
|---|---|
| 能 for 的一定是迭代器嗎 | 不一定,可能只是可迭代對象 |
| 迭代器能 for 嗎 | 能,因為迭代器的 __iter__ 返回自己 |
| iter(obj) 做了什么 | 調用 obj.__iter__(),拿到迭代器 |
| next(it) 做了什么 | 調用 it.__next__(),拿下一個值 |
| 取完后怎么結束 | Python 拋 StopIteration |
| 生成器是什么 | 用 yield 自動創(chuàng)建出來的迭代器 |
| 生成器會立刻執(zhí)行函數(shù)體嗎 | 不會,第一次 next() 才開始執(zhí)行 |
| 迭代器能重復遍歷嗎 | 通常不能,它會被消耗 |
到此這篇關于Python 迭代器與生成器的具體使用的文章就介紹到這了,更多相關Python 迭代器與生成器內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Python解析不規(guī)則JSON數(shù)據(jù)的實戰(zhàn)技巧
在真實世界的數(shù)據(jù)處理中,我們很少遇到教科書式的標準?JSON,文將為你提供一套從溫和修復到強力解析的完整工具箱,用?Python?輕松馴服這些野性數(shù)據(jù),有需要的小伙伴可以了解下2026-01-01
Python從Excel讀取數(shù)據(jù)并使用Matplotlib繪制成二維圖像
本課程實現(xiàn)使用 Python 從 Excel 讀取數(shù)據(jù),并使用 Matplotlib 繪制成二維圖像。這一過程中,將通過一系列操作來美化圖像,最終得到一個可以出版級別的圖像。本課程對于需要書寫實驗報告,學位論文,發(fā)表文章,做報告的學員具有較大價值2023-02-02
Python使用pandasai實現(xiàn)數(shù)據(jù)分析
本文主要介紹了Python使用pandasai實現(xiàn)數(shù)據(jù)分析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-06-06
使用Python構建一個完整的實時數(shù)據(jù)處理平臺
在當今數(shù)據(jù)驅動的時代,實時數(shù)據(jù)處理能力已成為企業(yè)核心競爭力之一,本文將介紹如何使用Python技術棧構建一個完整的實時數(shù)據(jù)處理平臺,涵蓋從數(shù)據(jù)采集、處理、存儲到可視化展示的全流程,需要的朋友可以參考下2026-02-02
python?pandas庫讀取excel/csv中指定行或列數(shù)據(jù)
通過閱讀表格,可以發(fā)現(xiàn)Pandas中提供了非常豐富的數(shù)據(jù)讀寫方法,下面這篇文章主要給大家介紹了關于python利用pandas庫讀取excel/csv中指定行或列數(shù)據(jù)的相關資料,需要的朋友可以參考下2022-02-02
Python +Selenium解決圖片驗證碼登錄或注冊問題(推薦)
這篇文章主要介紹了Python Selenium解決圖片驗證碼登錄或注冊問題,本文通過實例代碼給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02
Pandas數(shù)據(jù)結構中Series屬性詳解
本文主要介紹了Pandas數(shù)據(jù)結構中Series屬性詳解,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04

