1. 程式人生 > >python中的迭代器和生成器

python中的迭代器和生成器

在我們學習迭代器和生成器之前的時候,我們要先搞清楚幾個概念:

  • 「迭代協議:」 有__next__方法會前進道下一個結果,而且在一系列結果的末尾時,會引發StopIteration異常的物件.
  • 「可迭代物件:」 實現了__iter__方法的物件
  • 「迭代器:」 實現了__iter____next__方法的物件
  • 「生成器:」 通過生成器表示式或者yeild關鍵字實現的函式.

這裡不太好理解,我們借用一個圖

可迭代物件

需要注意的是可迭代物件不一定是迭代器.比如列表型別和字串型別都是可迭代物件,但是他們都不是迭代器.

In [1]: L1 = [1,2,3,4]

In [2]: type(L1)
Out[2]: list

In [3]: L1_iter=L1.__iter__()

In [4]: type(L1_iter)
Out[4]: list_iterator

但是對於容器以及檔案這樣的可迭代物件來說的話,他們都實現了一個__iter__方法. 這個方法可以返回一個迭代器.

迭代器中的__next__方法,next()方法和for語句

首先迭代器中都實現了__next__()方法. 我們可以直接呼叫迭代器的__next__方法來得到下一個值. 比如:

In [10]: L1_iter.__next__()
Out[10]: 1

In [11]: next(L1_iter)
Out[11]: 2

注意這裡,next()方法也是去呼叫迭代器內建的__next__方法. 所以這兩種操作是一樣的. 但是在日常使用的時候,我們不會直接去呼叫next()方法來使用生成器.

更多的操作是通過for語句來使用一個生成器.

就下面這兩段程式碼來看,其作用上是等效的.

L1 = [1, 2, 3, 4]

for x in L1:
print(x, end=" ")

print("\nthe same result of those two statements!")

L1_iter = L1.__iter__()
while True:
try:
x = L1_iter.__next__()
print(x, end=" ")
except StopIteration:
break

但是實際上,使用for語句在執行速度可能會更快一點. 因為迭代器在Python中是通過C語言實現的. 而while的方式則是以Python虛擬機器執行Python位元組碼的方式來執行的.

畢竟...你大爺永遠是你大爺. C語言永遠是你大爺...

列表解析式

列表解析式或者又叫列表生成式,這個東西就比較簡單了. 舉個簡單的例子,比如我們要定義一個1-9的列表. 我們可以寫L=[1,2,3,4,5,6,78,9] 同樣我們也可以寫L=[x for x in range(10)]

再舉一個簡單的例子,我們現在已經有一個列表L2=[1,2,3,4,5] 我們要得到每個數的平方的列表. 那麼我們有兩種做法:

L2 = [1, 2, 3, 4, 5]

# statement1
for i in range(len(L2)):
L2[i] = L2[i]*L2[i]
#statement2
L3 = [x*x for x in L2]

顯然從程式碼簡潔渡上來說 第二種寫法更勝一籌. 而且它的運算速度相對來說會更快一點(往往速度會快一倍.P.S.書上的原話,我沒有驗證...). 因為列表解析式式通過生成器來構造的,他們的迭代是python直譯器內部以C語言的速度來執行的. 特別是對於一些較大的資料集合,列表解析式的效能優點更加突出.

列表解析式還有一些高階的玩法. 比如可以與if語句配合使用:

L4 = [1, 2, 3, 4, 5, 6, 7, 8, 9]

L5 = [x for x in L4 if x % 2 == 0]

還可以使用for語句巢狀;

L6=[1,2,3,4,5]
L7=['a','b','c','d','e']

L8=[str(x)+y for x in L6 for y in L7]

或者可以寫的更長

L9=[(x,y) for x in range(5) if x % 2 ==0 for y in range(5) if y %2 ==1]

一個更復雜的例子

M = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
print(M[1][1])
print(M[1])
print([row[1] for row in M])
print([M[row][1] for row in (0,1,2)])
print([M[i][i] for i in range(len(M))])

同樣的,我們可以通過for語句來實現上述的功能. 但是列表解析式想對而言會更加簡潔.

另外map函式比等效的for迴圈要更快,而列表解析式往往會比map呼叫還要快一點.

python3中新的可迭代物件

python3相比如python2.x來說,它更強調迭代. 除了檔案,字典這樣的內建型別相關的迭代外. 字典方法keys,values都在python3中返回可迭代物件. 就像map,range,zip方法一樣. 它返回的並不是一個列表. 雖然從速度和記憶體佔用上更有優勢,但是有時候我們不得不使用list()方法使其一次性計算所有的結果.

range迭代器

In [12]: R=range(10)

In [13]: R
Out[13]: range(0, 10)

In [14]: I = iter(R)

In [15]: next(I)
Out[15]: 0

In [16]: R[5]
Out[16]: 5

In [17]: len(R)
Out[17]: 10

In [18]: next(I)
Out[18]: 1

range的話,僅支援迭代,len()和索引. 不支援其他的序列操作. 所以如果需要更多的列表工具的話,使用list()...

map,zip和filter迭代器

和range類似, map,zip和filter在python3.0中也轉變成了迭代器以節約記憶體空間. 但是它們和range又不一樣.(確切來說是range和它們不一樣) 它們不能在它們的結果上擁有在那些結果中保持不同位置的多個迭代器.(第四版書上原話,看看這叫人話嗎...)

翻譯一下就是,map,zip和filter返回的都是正經迭代器,不支援len()和索引. 以map為例做個對比.

In [20]: map_abs = map(abs,[1,-3,4])

In [21]: M1 = iter(map_abs)

In [22]: M2=iter(map_abs)

In [23]: next(M1)
Out[23]: 1

In [24]: next(M2)
Out[24]: 3

而range不是正經的迭代器. 它支援在其結果上建立多個活躍的迭代器.

In [25]: R=range(10)

In [26]: r1 = iter(R)

In [27]: r2=iter(R)

In [28]: next(r1)
Out[28]: 0

In [29]: next(r2)
Out[29]: 0

字典中的迭代器

同樣的,python3中字典的keys,values和items方法返回的都是可迭代物件.而非列表.

In [30]: D = dict(a=1,b=2,c=3)

In [31]: D
Out[31]: {'a': 1, 'b': 2, 'c': 3}

In [32]: K = D.keys()

In [33]: K
Out[33]: dict_keys(['a', 'b', 'c'])

In [34]: next(K)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-34-02c2ef8731e9> in <module>
----> 1 next(K)

TypeError: 'dict_keys' object is not an iterator

In [35]: i = iter(K)

In [36]: next(i)
Out[36]: 'a'

In [37]: for k in D.keys(): print(k,end=' ')

同樣的,我們可以利用list()函式來顯式的把他們變成列表. 另外,python3中的字典仍然有自己的迭代器. 它返回連續的見. 因此在遍歷的時候,無需顯式的呼叫keys().

In [38]: for key in D: print(key,end=' ')
a b c

生成器

生成器可以說是迭代器的一個子集. 有兩種建立方法:

  • 生成器函式: 編寫常規的以def為關鍵字的函式,使用yield關鍵字返回結果. 在每個結果之間掛起和繼續他們的狀態.
  • 生成器表示式: 類似前面所說的列表解析式.

生成器函式關鍵字 yeild

「狀態掛起」

和返回一個值並且退出的常規函式不同,生成器函式自動在生成值得時刻掛起並繼續函式的執行. 它在掛起時會儲存包括整個本地作用域在內的所有狀態. 在恢復執行時,本地變數資訊依舊可用.

生成器函式使用yeild語句來掛起函式並想呼叫者傳送回一個值.之後掛起自己. 在恢復執行的時候,生成器函式會從它離開的地方繼續執行.

「生成器函式的應用」

def gensquares(num):
for i in range(num):
yield i**2

for i in gensquares(5):
print(i)

同樣的,生成器其實也是實現了迭代器協議. 提供了內建的__next__方法.

上面這個例子如果我們要改寫為普通函式的話,可以寫成如下的樣子.

def buildsquares(num):
res = []
for i in range(num):
res.append(i**2)
return res

for i in buildsquares(5):
print(i)

看上去實現的功能都是一樣的. 但是區別在於 生成器的方式產生的是一個惰性計算序列. 在呼叫時才進行計算得出下一個值. 而第二種常規函式的方式,是先計算得出所有結果返回一個列表. 從記憶體佔用的角度來說,生成器函式的方式更優一點.

生成器表示式

與列表解析式差不多. 生成器表示式用來構造一些邏輯相對簡單的生成器. 比如

g = (x**2 for x in range(4))

在使用時可以通過next()函式或者for迴圈進行呼叫.

實戰:改寫map和zip函式

改寫map函式

「一年級版本:」

def mymap(func,*seqs):
res=[]
print(list(zip(*seqs)))
for args in zip(*seqs):
res.append(func(*args))
return res

「二年級版本:」

def mymap(func,*seqs):    
return [func(*args) for args in zip(*seqs)]

「三年級版本:」

def mymap(func,*seqs):
res=[]
for args in zip(*seqs):
yield func(*args)


print(list(mymap(abs,[-1,-2,1,2,3])))
print(list(mymap(pow,[1,2,3],[2,3,4,5])))

「小學畢業班版本」

def mymap(func,*seqs):
return (func(*args) for args in zip(*seqs))

改寫zip函式

「一年級版本」

def myzip(*seqs):
seqs = [list(S) for S in seqs]
print(seqs)
res = []
while all(seqs):
res.append(tuple(S.pop(0) for S in seqs))
return res


print(myzip('abc', 'xyz'))

知識點: all()函式和any()函式. all()函式,如果可迭代物件中的所有元素都為True或者可迭代物件為None. 則返回True. any()函式,可迭代物件中的任一元素為True則返回True.,如果迭代器為空,則返回False.

「二年級版本」

def myzip(*seqs):
seqs = [list(S) for S in seqs]
while all(seqs):
yield tuple(S.pop(0) for S in seqs)

print(list(myzip('abc', 'xyz')))

參考資料:

  • Iterables vs. Iterators vs. Generators
  • Python學習手冊(第五版)