python可迭代物件,迭代器和生成器
迭代是資料處理的基石,掃描記憶體中放不下的資料集時,我們要找到一種惰性獲取資料項的方式,即按需一次獲取一個數據項,這就是迭代器模式。
所有的生成器都是迭代器,因為生成器實現了迭代器的介面,迭代器用於從集合中取出元素,生成器用於憑空生成元素。
所有的序列都可以迭代:
序列可以迭代的原因:iter函式
直譯器需要迭代物件x的時候,會自動呼叫iter(x)函式,這個iter函式有這些作用:
- 檢查物件是否實現了__iter__方法,如果實現了就呼叫它,獲取一個迭代器。
- 如果沒有iter方法,但是實現了__getitem__方法,那麼會建立一個迭代器,嘗試按照順序從0開始獲取元素。
def __getitem__(self, index):
return self.words[index]
- 如果上面的方法都嘗試失敗,會丟擲TypeError異常
所以這樣的話,迭代物件之前顯式地檢查物件是否可迭代是沒有必要的,畢竟嘗試迭代不可迭代的物件的時候,python丟擲的異常資訊是非常明顯的:TypeError: C object is not iterable
綜上所述:
可迭代的物件定義:
如果物件實現了能返回迭代器的__iter__方法,那麼該物件就是可迭代物件;序列都是可迭代物件;如果物件實現了__getitem__方法,並且引數是從0開始的,那麼該物件也是可迭代的。
所以可迭代物件和迭代器之間的關係是:python從可迭代物件中獲取迭代器
標準的迭代器介面有兩個:
- next:
返回下一個可用元素,如果沒有元素,丟擲StopIteration異常 - iter:
返回self,用於在應該使用可迭代物件的地方使用迭代器,例如for迴圈。
這是迭代期間發生的基本細節
>>> items = [1, 2, 3] >>> # Get the iterator >>> it = iter(items) # Invokes items.__iter__() >>> # Run the iterator >>> next(it) # Invokes it.__next__() 1 >>> next(it) 2 >>> next(it) 3 >>> next(it) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
迭代器的定義:
迭代器是這樣的物件,實現了無引數的__next__方法,返回序列中的下一個元素,如果沒有元素了,那麼就丟擲StopIteration異常。此外,迭代器中也實現了__iter__方法,所以迭代器也可以迭代。迭代器是不可逆的!!!
Python的迭代器協議需要__iter__方法返回一個實現了__next__方法的迭代器物件
重點概念:
可迭代的物件一定不能是自身的迭代器,所以,可迭代物件必須實現__iter__方法,但是不能實現__next__方法。
迭代器應該一直可以迭代,迭代器的__iter__應該返回它本身。
- 可迭代物件的__iter__方法生成它的迭代器:
def __iter__(self):
return Iterator(self.words)
- 迭代器的__iter__方法返回它本身:
def __iter__(self):
return self
如果我們有一個自定義容器物件,裡面包含有列表元組等其他的迭代物件,我們想在這個自定義物件上面執行迭代操作該怎麼辦呢?
class Node:
def __init__(self, value):
self._value = value
self._children = []
def __repr__(self):
return 'Node({!r})'.format(self._value)
def add_child(self, node):
self._children.append(node)
def __iter__(self):
return iter(self._children)
# Example
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
# Outputs Node(1), Node(2)
for ch in root:
print(ch)
解決方法:定義一個__iter__()方法,將迭代操作代理到容器內部的物件上面去,這裡的iter()函式只是使用簡化程式碼,就跟len(s)跟使用s.len()方法是一樣的。
生成器:
當你想實現一個新的迭代模式,跟普通的內建函式比如range(),reversed()不一樣的時候,就需要使用一個生成器函式來定義它。
生成器的__iter__方法:
def __iter__(self):
for word in self.words:
yield word
return
只要python函式的定義體中含有yield關鍵字,那麼這個函式就是生成器函式
但是這個生成器函式和普通函式不同的是生成器只能用於迭代操作!!
呼叫next(生成器函式)會獲取yield生成的下一個元素,生成器函式的定義體執行完畢之後,生成器物件會丟擲StopIteration異常
使用生成器建立新的迭代模式:
如果想實現一個新的迭代模式,可以使用一個生成器來定義它:
# 這是實現某個範圍內浮點數的生成器
def frange(start, stop, increment):
x = start
while x < stop:
yield x
x += increment
為了使用這個函式,可以使用for迴圈來迭代它或者使用其他的界都一個可迭代物件的函式:
1. for n in frange(0, 4, 0.5):
... print(n)
2. list(frange(0, 1, 0.125))
下面用一個例子來描述生成器的工作機制:
>>> def countdown(n):
... print('Starting to count from', n)
... while n > 0:
... yield n
... n -= 1
... print('Done!')
>>> c = countdown(3) # 建立一個生成器
>>> c
<generator object countdown at 0x1006a0af0>
>>> next(c) # 執行到第一次yield,也就是3
Starting to count from 3
3
>>> # Run to the next yield
>>> next(c)
2
>>> # Run to next yield
>>> next(c)
1
>>> # Run to next yield (iteration stops)
>>> next(c)
Done!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
生成器表示式:
生成器表示式可以理解為列表推導的惰性版本,不會迫切構建列表,而是會返回一個生成器。
def gen_AB():
print('start')
yield 'A'
print('continue')
yield 'B'
print('end')
-------------------------------------------------
res1 = [x*3 for x in gen_ab()] # 這是列表推導迫切的迭代gen_AB()函式生成的生成器
start
continue
end
for i in res1:
print(i)
AAA
BBB
--------------------------------------------------
res2 = (x*3 for x in gen_ab()) # res2是一個生成器物件,只有for迴圈迭代res2時,gen_AB()函式的定義體才能真正執行
for i in res2:
print(i)
start
AAA
continue
BBB
end
構建能支援迭代協議的自定義物件
在一個物件上實現迭代最簡單的方式是使用一個生成器函式,下面的例子是以深度優先的方式遍歷樹形節點的生成器:
class Node:
def __init__(self, value):
self._value = value
self._children = []
def __repr__(self):
return 'Node({!r})'.format(self._value)
def add_child(self, node):
self._children.append(node)
def __iter__(self): # 該物件有__iter__屬性,所以該物件是一個可迭代物件
return iter(self._children)
def depth_first(self): # 這裡是一個生成器
yield self # 首先返回節點本身,再返回它的子節點
for c in self:
yield from c.depth_first()
# Example
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))
for ch in root.depth_first(): # 這裡證明了該自定義物件可迭代!!!
print(ch)
# Outputs Node(0), Node(1), Node(3), Node(4), Node(2), Node(5)
python的迭代協議要求__iter__能返回一個特殊的迭代器物件,這個迭代器物件實現了__next__方法,但是真的實現這樣的東西會非常的複雜,因為迭代器必須在迭代過程中維護大量的狀態資訊,但是將迭代器定義為一個生成器後,問題就簡單多了。
反向迭代:
首先可以使用內建的reversed()函式,但是反向迭代僅僅當物件的大小可預先確定或者物件實現了 reversed() 的特殊方法時才能生效。如果物件不符合這種特性,那麼需要將物件轉換成一個列表才可以,但是這樣轉換需要大量的記憶體。
還有一種方法就是在自定義類裡面實現__reversed__來實現反向迭代。
class Countdown:
def __init__(self, start):
self.start = start
def __iter__(self): # 正向迭代
n = self.start
while n > 0:
yield n
n -= 1
def __reversed__(self): # 反向迭代
n = 1
while n <= self.start:
yield n
n += 1
for rr in reversed(Countdown(30)): # 反向迭代實現成功
print(rr)
for rr in Countdown(30):
print(rr)
帶有外部狀態的生成器函式
目的是做一個生成器函式暴露外部狀態給使用者,方法是將它實現為一個類,然後把生成器函式放到__iter__方法中,這樣不會改變任何的演算法邏輯,由於這是類中的一個方法,所以可以提供各種方法和屬性來供使用者使用:
如果在做迭代操作的時候不使用for迴圈,那麼需要先呼叫iter()函式來生成迭代器,然後再使用next()操作
from collections import deque
class linehistory:
def __init__(self, lines, histlen=3):
self.lines = lines
self.history = deque(maxlen=histlen)
def __iter__(self):
for lineno, line in enumerate(self.lines, 1):
self.history.append((lineno, line))
yield line
def clear(self):
self.history.clear()
那麼怎麼使用這個類呢?
with open('somefile.txt') as f:
lines = linehistory(f)
for line in lines:
if 'python' in line:
for lineno, hline in lines.history:
print('{}:{}'.format(lineno, hline), end='')
迭代器和生成器切片:
如果想得到一個迭代器生成的切片物件,但是標準的切片方法不能實現這個功能,我們就使用itertools.islice()來對迭代器和生成器做切片操作:
>>> def count(n):
... while True:
... yield n
... n += 1
...
>>> c = count(0)
>>> c[10:20] # 這裡並不能使用普通的切片方法,因為迭代器的長度我們事先不知道
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable
>>> # Now using islice()
>>> import itertools
>>> for x in itertools.islice(c, 10, 20): # 這個函式返回一個可以生成指定元素的迭代器,通過遍歷並丟棄直到切片開始索引位置。
... print(x)
10
...
19
>>>
跳過可迭代物件的部分內容
itertools.dropwhile()
函式可以傳遞一個函式物件和一個可迭代物件,它會返回一個迭代器物件,丟棄原有序列中直到函式返回Flase之前的所有元素,然後返回後面所有元素,所以就是丟棄前面所有返回True的元素,直到第一個False出現。
如果想跳過開始部分的註釋行的話,可以使用下面的方法:
>>> from itertools import dropwhile
>>> with open('/etc/passwd') as f:
... for line in dropwhile(lambda line: line.startswith('#'), f):
... print(line, end='')
排列組合的迭代
如果你想遍歷一個集合中元素的所有可能的排列組合,可以使用下面的方法:
>>> items = ['a', 'b', 'c']
>>> from itertools import permutations
>>> for p in permutations(items):
... print(p)
...
('a', 'b', 'c')
('a', 'c', 'b')
('b', 'a', 'c')
('b', 'c', 'a')
('c', 'a', 'b')
('c', 'b', 'a')
>>>
itertools.permutations()函式接受一個集合併產生一個元組序列,每個元組都是集合中所有元素的一個可能的排列組合。如果想對所有排列組合指定長度,可以再傳遞一個引數:for p in permutations(items, 2):
使用 itertools.combinations() 可得到輸入集合中元素的所有的組合:
>>> for c in combinations(items, 2):
... print(c)
...
('a', 'b')
('a', 'c')
('b', 'c')
這個函式不區別對待元素的順序,所以ab和ba是一樣的,只會輸出一個。
所以當我們遇到比較複雜的迭代問題的時候,我們都可以去itertools模組裡面看一看!!!
迭代中得到元素索引值
如果在迭代過程中想跟蹤正在被處理的元素的索引值,我們可以使用內建的enumerate()函式來處理
>>> my_list = ['a', 'b', 'c']
>>> for idx, val in enumerate(my_list):
... print(idx, val)
...
0 a
1 b
2 c
如果想從1開始計數,我們可以在my_list後面傳遞一個引數1。
enumerate()函式返回一個enumerate物件例項,這個物件例項是一個迭代器,回連續的包含一個計數和一個值的元組。
同時迭代多個序列
如果你想同事迭代兩個序列,每次分別從每個序列中取出一個元素,那麼我們可以使用zip()函式:
>>> xpts = [1, 5, 4, 2, 10, 7]
>>> ypts = [101, 78, 37, 15, 62, 99]
>>> for x, y in zip(xpts, ypts):
... print(x,y)
...
1 101
一旦其中某個序列到底結尾了,那麼迭代就宣告結束,但是如果我們想以長序列來結束,我們可以使用itertools.zip_longest()
在處理成對資料的時候,zip()函式是非常有用的,比如你有兩個列表,分別是鍵列表和值列表,那麼我們可以這樣:
headers = ['name', 'shares', 'price']
values = ['ACME', 100, 490.1]
s = dict(zip(headers,values))
展開有巢狀的序列–陣列拍平
可以使用一個yield from的遞迴生成器來實現:
from collections import Iterable
def flatten(items, ignore_types=(str, bytes)):
for x in items:
if isinstance(x, Iterable) and not isinstance(x, ignore_types):
# 前面這個判斷用於判斷元素是否可迭代
# 後面的這個判斷用來將字串和位元組排除在可迭代物件之外
yield from flatten(x)
else:
yield x
items = [1, 2, [3, 4, [5, 6], 7], 8]
# Produces 1 2 3 4 5 6 7 8
for x in flatten(items):
print(x)
迭代器代替while無限迴圈
在程式碼中使用while迴圈來迭代處理是因為它需要呼叫某個函式和其他迭代方式不同的測試條件。
首先展示一個常見的while迴圈:
CHUNKSIZE = 8192
def reader(s):
while True:
data = s.recv(CHUNKSIZE)
if data == b'':
break
process_data(data)
然後使用迭代器實現的:
def reader2(s):
for chunk in iter(lambda: s.recv(CHUNKSIZE), b''):
# 這算是iter()一個鮮為人知的特性,可以選擇一個callable物件和一個結尾標記作為引數。
# 它會生產一個迭代器,不斷呼叫這個callable物件,直到返回值和這個結尾標記一樣
pass
# process_data(data)