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

