原文來自廖雪峰Python3教程高級特性部分整胃,略有增刪改赋秀。
我整理本文的目的是有這樣的問題:在Python中,for循環(huán)極為常用待榔,我們利用它進行批量提取數(shù)據(jù)或進行數(shù)據(jù)運算逞壁。但較為深入的概念和機制我們是否真正有所理解呢?
可迭代對象與迭代器
可以直接作用于for
循環(huán)的數(shù)據(jù)類型有以下幾種:
- 一類是集合數(shù)據(jù)類型,如
list
猾担、tuple
袭灯、dict
、set
绑嘹、str
等稽荧; - 一類是
generator
,包括生成器和帶yield
的生成器函數(shù)工腋。
這些可以直接作用于for
循環(huán)的對象統(tǒng)稱為可迭代對象:Iterable
姨丈。
可以使用isinstance()
判斷一個對象是否是Iterable
對象:
>>> from collections import Iterable
>>> isinstance([], Iterable)
True
>>> isinstance({}, Iterable)
True
>>> isinstance('abc', Iterable)
True
>>> isinstance((x for x in range(10)), Iterable)
True
>>> isinstance(100, Iterable)
False
可以被next()
函數(shù)調(diào)用并不斷返回下一個值的對象稱為迭代器:Iterator
。
顯然可迭代對象與迭代器是兩個不同的概念擅腰,它們的區(qū)別需要通過生成器加以理解蟋恬。
生成器
通過列表生成式,我們可以直接創(chuàng)建一個列表趁冈。但是歼争,受到內(nèi)存限制,列表容量肯定是有限的渗勘。而且沐绒,創(chuàng)建一個包含100萬個元素的列表,不僅占用很大的存儲空間旺坠,如果我們僅僅需要訪問前面幾個元素乔遮,那后面絕大多數(shù)元素占用的空間都白白浪費了。
因此取刃,Python實現(xiàn)了一種一邊循環(huán)一邊計算的機制蹋肮,稱為生成器:generator。相比于列表生成式直接生成數(shù)據(jù)璧疗,生成器存儲循環(huán)推演算法以及當前值坯辩。
創(chuàng)建生成器的一個簡單方法是把一個列表生成式的[]
改成()
。
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>
創(chuàng)建L
和g
的區(qū)別僅在于最外層的[]
和()
崩侠,L
是一個list漆魔,而g
是一個generator。
g
默認輸出對象信息啦膜,需要通過next()
函數(shù)獲得generator的下一個返回值:
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4
>>> next(g)
9
>>> next(g)
16
>>> next(g)
25
>>> next(g)
36
>>> next(g)
49
>>> next(g)
64
>>> next(g)
81
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
我們講過,generator保存的是算法淌喻,每次調(diào)用next(g)
僧家,就可以通過當前的元素值計算出g
的下一個元素的值,直到計算到最后一個元素裸删,沒有更多的元素時八拱,拋出StopIteration
的錯誤。
顯然這種操作不實用,根據(jù)上一節(jié)的介紹肌稻,生成器也是可迭代對象清蚀,因此可以使用for循環(huán)。
>>> g = (x * x for x in range(10))
>>> for n in g:
... print(n)
...
0
1
4
9
16
25
36
49
64
81
所以爹谭,我們創(chuàng)建了一個generator后枷邪,基本上永遠不會調(diào)用next()
,而是通過for
循環(huán)來迭代它诺凡,并且不需要關(guān)心StopIteration
的錯誤东揣。
生成器函數(shù)
generator非常強大。如果推算的算法比較復雜腹泌,用類似列表生成式的for
循環(huán)無法實現(xiàn)的時候嘶卧,還可以用函數(shù)來實現(xiàn)。
比如凉袱,著名的斐波拉契數(shù)列(Fibonacci)芥吟,除第一個和第二個數(shù)外,任意一個數(shù)都可由前兩個數(shù)相加得到:
1, 1, 2, 3, 5, 8, 13, 21, 34, ...
斐波拉契數(shù)列用列表生成式寫不出來专甩,但是钟鸵,用函數(shù)把它打印出來卻很容易:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
print(b)
a, b = b, a + b
n = n + 1
return 'done'
上面的函數(shù)可以輸出斐波那契數(shù)列的前N個數(shù):
>>> fib(6)
1
1
2
3
5
8
'done'
仔細觀察,可以看出配深,fib
函數(shù)實際上是定義了斐波拉契數(shù)列的推算規(guī)則携添,可以從第一個元素開始,推算出后續(xù)任意的元素篓叶,這種邏輯其實非常類似generator烈掠。
也就是說,上面的函數(shù)和generator僅一步之遙缸托。要把fib
函數(shù)變成generator左敌,只需要把print(b)
改為yield b
就可以了:
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'
這就是定義generator的另一種方法。如果一個函數(shù)定義中包含yield
關(guān)鍵字俐镐,那么這個函數(shù)就不再是一個普通函數(shù)矫限,而是一個generator:
>>> f = fib(6)
>>> f
<generator object fib at 0x104feaaa0>
這里,最難理解的就是generator和函數(shù)的執(zhí)行流程不一樣佩抹。函數(shù)是順序執(zhí)行叼风,遇到return
語句或者最后一行函數(shù)語句就返回。而變成generator的函數(shù)棍苹,在每次調(diào)用next()
的時候執(zhí)行无宿,遇到yield
語句返回,再次執(zhí)行時從上次返回的yield
語句處繼續(xù)執(zhí)行枢里。
舉個簡單的例子孽鸡,定義一個generator蹂午,依次返回數(shù)字1,3彬碱,5:
def odd():
print('step 1')
yield 1
print('step 2')
yield(3)
print('step 3')
yield(5)
調(diào)用該generator時豆胸,首先要生成一個generator對象,然后用next()
函數(shù)不斷獲得下一個返回值:
>>> o = odd()
>>> next(o)
step 1
1
>>> next(o)
step 2
3
>>> next(o)
step 3
5
>>> next(o)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
可以看到巷疼,odd
不是普通函數(shù)晚胡,而是generator,在執(zhí)行過程中皮迟,遇到yield
就中斷搬泥,下次又繼續(xù)執(zhí)行。執(zhí)行3次yield
后伏尼,已經(jīng)沒有yield
可以執(zhí)行了忿檩,所以,第4次調(diào)用next(o)
就報錯爆阶。
但是用for
循環(huán)調(diào)用generator時燥透,發(fā)現(xiàn)拿不到generator的return
語句的返回值。如果想要拿到返回值辨图,必須捕獲StopIteration
錯誤班套,返回值包含在StopIteration
的value
中:
>>> g = fib(6)
>>> while True:
... try:
... x = next(g)
... print('g:', x)
... except StopIteration as e:
... print('Generator return value:', e.value)
... break
...
g: 1
g: 1
g: 2
g: 3
g: 5
g: 8
Generator return value: done
相互關(guān)系
可以使用isinstance()
判斷一個對象是否是Iterator
對象:
>>> from collections import Iterator
>>> isinstance((x for x in range(10)), Iterator) # 這是一個生成器
True
>>> isinstance([], Iterator)
False
>>> isinstance({}, Iterator)
False
>>> isinstance('abc', Iterator)
False
生成器都是Iterator
對象,但list
故河、dict
吱韭、str
雖然是Iterable
,卻不是Iterator
鱼的。
把list
理盆、dict
、str
等Iterable
變成Iterator
可以使用iter()
函數(shù):
>>> isinstance(iter([]), Iterator)
True
>>> isinstance(iter('abc'), Iterator)
True
為什么list
凑阶、dict
猿规、str
等數(shù)據(jù)類型不是Iterator
?
這是因為Python的Iterator
對象表示的是一個數(shù)據(jù)流宙橱,Iterator
對象可以被next()
函數(shù)調(diào)用并不斷返回下一個數(shù)據(jù)姨俩,直到?jīng)]有數(shù)據(jù)時拋出StopIteration
錯誤。可以把這個數(shù)據(jù)流看做是一個有序序列师郑,但我們卻不能提前知道序列的長度环葵,只能不斷通過next()
函數(shù)實現(xiàn)按需計算下一個數(shù)據(jù),所以Iterator
的計算是惰性的宝冕,只有在需要返回下一個數(shù)據(jù)時它才會計算张遭。
Iterator
甚至可以表示一個無限大的數(shù)據(jù)流,例如全體自然數(shù)猬仁。而使用list是永遠不可能存儲全體自然數(shù)的帝璧。
小結(jié)
凡是可作用于for
循環(huán)的對象都是可迭代類型;
凡是可作用于next()
函數(shù)的對象都是迭代器類型湿刽,它們表示一個惰性計算的序列的烁;
集合數(shù)據(jù)類型如list
、dict
诈闺、str
等是Iterable
但不是Iterator
性宏,不過可以通過iter()
函數(shù)獲得一個Iterator
對象纯露。
Python的for
循環(huán)本質(zhì)上就是通過不斷調(diào)用next()
函數(shù)實現(xiàn)的,例如:
for x in [1, 2, 3, 4, 5]:
pass
實際上完全等價于:
# 首先獲得Iterator對象:
it = iter([1, 2, 3, 4, 5])
# 循環(huán):
while True:
try:
# 獲得下一個值:
x = next(it)
except StopIteration:
# 遇到StopIteration就退出循環(huán)
break