迭代是數據處理的基石。掃描內存中放不下的數據集時,我們要找到一種惰性獲取數據項的方式,即按需一次獲取一個數據項。這就是迭代器模式(Iterator pattern)。
Sentence類第1版:單詞序列
我們要實現一個 Sentence 類,以此打開探索可迭代對象的旅程。我們向這個類的構造方法傳入包含一些文本的字符串,然后可以逐個單詞迭代。第 1 版要實現序列協議,這個類的對象可以迭代,因為所有序列都可以迭代——這一點前面已經說過,不過現在要說明真正的原因。
🌰 sentence.py:把句子划分為單詞序列
1 class Sentence: 2 3 def __init__(self, text): 4 self.text = text #傳進來的參數 5 self.words = RE_WORD.findall(self.text) #匹配到的字符編程列表形式 6 7 def __len__(self): #支持查看長度 8 return len(self.words) 9 10 def __getitem__(self, position): #支持索引取值 11 return self.words[position] 12 13 def __repr__(self): #供程序猿查看的~ 14 return 'Sentence(%s)' % reprlib.repr(self.text)
注意:
默認情況下,reprlib.repr 函數生成的字符串最多有 30 個字符
以上代碼執行的結果為:
>>> s = Sentence('"The time has come," the Walrus said,') # 實例化Sentence並傳入需要查找的字符串 >>> s Sentence('"The time ha... Walrus said,') # __repr__中的reprlib實現的縮寫 >>> for word in s: # _getitem__實現的可以迭代 ... print(word) The time has come the Walrus said >>> list(s) # 轉換成列表 ['The', 'time', 'has', 'come', 'the', 'Walrus', 'said']
序列可以迭代的原因:iter函數
解釋器需要迭代對象 x 時,會自動調用 iter(x)。
內置的 iter 函數有以下作用。
(1) 檢查對象是否實現了 __iter__ 方法,如果實現了就調用它,獲取一個迭代器。
(2) 如果沒有實現 __iter__ 方法,但是實現了 __getitem__ 方法,Python 會創建一個迭代器,嘗試按順序(從索引 0 開始)獲取元素。
(3) 如果嘗試失敗,Python 拋出 TypeError 異常,通常會提示“C objectis not iterable”(C 對象不可迭代),其中 C 是目標對象所屬的類。
任何 Python 序列都可迭代的原因是,它們都實現了 __getitem__ 方法。其實,標准的序列也都實現了 __iter__ 方法,因此你也應該這么做
這是鴨子類型(duck typing)的極端形式:不僅要實現特殊的 __iter__ 方法,還要實現 __getitem__ 方法,而且__getitem__ 方法的參數是從 0 開始的整數(int),這樣才認為對象是可迭代的。
在白鵝類型(goose-typing)理論中,可迭代對象的定義簡單一些,不過沒那么靈活:如果實現了 __iter__ 方法,那么就認為對象是可迭代的。此時,不需要創建子類,也不用注冊,因為 abc.Iterable 類實現了 __subclasshook__ 方法,請看下面的 🌰
>>> class Foo: ... def __iter__(self): ... pass ... >>> f = Foo() >>> from collections import abc >>> issubclass(Foo, abc.Iterable) True >>> isinstance(f, abc.Iterable) True
可迭代的對象與迭代器的對比
可迭代的對象
使用 iter 內置函數可以獲取迭代器的對象。如果對象實現了能返回迭代器的 __iter__ 方法,那么對象就是可迭代的。序列都可以迭代;實現了 __getitem__ 方法,而且其參數是從零開始的索引,這種對象也可以迭代。
🌰 下面是一個簡單的 for 循環,迭代一個字符串。這里,字符串 'ABC'是可迭代的對象。背后是有迭代器的,只不過我們看不到:
>>> s = 'demon' >>> for char in s: ... print(char) ... d e m o n
如果沒有 for 語句,不得不使用 while 循環模擬,要像下面這樣寫:
1 s = 'demon' 2 it = iter(s) #使用迭代器迭代s字符串 3 4 while True: 5 try: 6 print(next(it)) #調用next等通過從iter中獲取值 7 except StopIteration: #當迭代器中的所有數據都迭代完畢以后,會報StopIteration的異常 8 del it 9 break #刪除迭代器並跳出循環體
標准的迭代器接口有兩個方法。
__next__
返回下一個可用的元素,如果沒有元素了,拋出 StopIteration異常。
__iter__
返回 self,以便在應該使用可迭代對象的地方使用迭代器,例如在 for 循環中。
這個接口在 collections.abc.Iterator 抽象基類中制定。這個類定義了 __next__ 抽象方法,而且繼承自 Iterable 類;__iter__ 抽象方法則在 Iterable 類中定義。如圖所示。
Iterable 和 Iterator 抽象基類。以斜體顯示的是抽象方法。具體的 Iterable.__iter__ 方法應該返回一個 Iterator 實例。具體的 Iterator 類必須實現 __next__ 方法。Iterator.__iter__ 方法直接返回實例本身。
看下面的 🌰
1 import re 2 import reprlib 3 4 RE_WORD = re.compile(r'\w+') #需要匹配的正則 5 6 class Sentence: 7 8 def __init__(self, text): 9 self.text = text #傳進來的參數 10 self.words = RE_WORD.findall(self.text) #匹配到的字符編程列表形式 11 12 def __len__(self): #支持查看長度 13 return len(self.words) 14 15 def __getitem__(self, position): #支持索引取值 16 return self.words[position] 17 18 def __repr__(self): #供程序猿查看的~ 19 return 'Sentence(%s)' % reprlib.repr(self.text) 20 21 s3 = Sentence('Pig and Pepper') #實例化 22 it = iter(s3) #從s3中獲取迭代器的內容 23 print('iter迭代器:', it) #打印迭代器 24 print(next(it)) #獲取迭代器 25 print(next(it)) 26 print(next(it)) 27 #print(next(it)) #迭代器里面的內容已經空了,在取就會報錯了,StopIteration 28 print('s3變成序列:', list(it)) #把s3實例變成序列,因為迭代器內容沒有了,所以是空列表 29 print('s3重新獲取序列:', list(iter(s3)))
因為迭代器只需 __next__ 和 __iter__ 兩個方法,所以除了調用next() 方法,以及捕獲 StopIteration 異常之外,沒有辦法檢查是否還有遺留的元素。此外,也沒有辦法“還原”迭代器。如果想再次迭代,那就要調用 iter(...),傳入之前構建迭代器的可迭代對象。傳入迭代器本身沒用,因為前面說過 Iterator.__iter__ 方法的實現方式是返回實例本身,所以傳入迭代器無法還原已經耗盡的迭代器。
Sentence類第2版:典型的迭代器
Sentence 類可以迭代,因為它實現了特殊的__iter__ 方法,構建並返回一個 SentenceIterator 實例。《設計模式:可復用面向對象軟件的基礎》一書就是這樣描述迭代器設計模式的。這里之所以這么做,是為了清楚地說明可迭代的對象和迭代器之間的重要區別,以及二者之間的聯系。
sentence_iter.py:使用迭代器模式實現 Sentence 類
1 import re 2 import reprlib 3 4 RE_WORD = re.compile(r'\w+') 5 6 7 class Sentence: 8 9 def __init__(self, text): 10 self.text = text 11 self.words = RE_WORD.findall(self.text) 12 13 def __repr__(self): 14 return 'Sentence(%s)' % reprlib.repr(self.text) 15 16 def __iter__(self): #可以迭代 17 return SentenceIterator(self.words) #返回一個迭代器 18 19 20 class SentenceIterator: 21 22 def __init__(self, words): #SentenceIterator 實例引用單詞列表 23 self.words = words 24 self.index = 0 #self.index 用於確定下一個要獲取的單詞 25 26 def __next__(self): 27 try: 28 word = self.words[self.index] #獲取 self.index 索引位上的單詞 29 except IndexError: #獲取的索引上沒有值,就代表迭代器悶得蜜了吧~ 30 raise StopIteration() #結束了,當然是拋出StopIteration的異常咯 31 self.index += 1 #要么就讓索引自動+1供下次迭代器調用使用 32 return word #返回索引上面的值 33 34 def __iter__(self): #實現__iter__方法 35 return self
注意,對這個示例來說,其實沒必要在 SentenceIterator 類中實現__iter__ 方法,不過這么做是對的,因為迭代器應該實現 __next__和 __iter__ 兩個方法,而且這么做能讓迭代器通過issubclass(SentenceInterator, abc.Iterator) 測試。如果讓SentenceIterator 類繼承 abc.Iterator 類,那么它會繼承abc.Iterator.__iter__ 這個具體方法。
把Sentence變成迭代器:壞主意
構建可迭代的對象和迭代器時經常會出現錯誤,原因是混淆了二者。要知道,可迭代的對象有個 __iter__ 方法,每次都實例化一個新的迭代器;而迭代器要實現 __next__ 方法,返回單個元素,此外還要實現__iter__ 方法,返回迭代器本身。
除了 __iter__ 方法之外,你可能還想在 Sentence 類中實現__next__ 方法,讓 Sentence 實例既是可迭代的對象,也是自身的迭代器。可是,這種想法非常糟糕。根據有大量 Python 代碼審查經驗的Alex Martelli 所說,這也是常見的反模式。
迭代器模式可用來:
-
訪問一個聚合對象的內容而無需暴露它的內部表示
-
支持對聚合對象的多種遍歷
-
為遍歷不同的聚合結構提供一個統一的接口(即支持多態迭代)為了“支持多種遍歷”,必須能從同一個可迭代的實例中獲取多個獨立的迭代器,而且各個迭代器要能維護自身的內部狀態,因此這一模式正確的實現方式是,每次調用 iter(my_iterable) 都新建一個獨立的迭代器。這就是為什么這個示例需要定義 SentenceIterator 類。
Sentence類第3版:生成器函數實
實現相同功能,但卻符合 Python 習慣的方式是,用生成器函數代替SentenceIterator 類
sentence_gen.py:使用生成器函數實現 Sentence 類
1 import re 2 import reprlib 3 4 RE_WORD = re.compile(r'\w+') 5 6 7 class Sentence: 8 9 def __init__(self, text): 10 self.text = text 11 self.words = RE_WORD.findall(self.text) 12 13 def __repr__(self): 14 return 'Sentence(%s)' % reprlib.repr(self.text) 15 16 def __iter__(self): 17 for word in self.words: #迭代self.words 18 yield word #產出當前的word 19 return #這個 return 語句不是必要的;這個函數可以直接“落空”,自動返回。 20 #不管有沒有 return 語句,生成器函數都不會拋出 StopIteration
生成器函數的工作原理
只要 Python 函數的定義體中有 yield 關鍵字,該函數就是生成器函數。調用生成器函數時,會返回一個生成器對象。也就是說,生成器函數是生成器工廠。
注意:
普通的函數與生成器函數在句法上唯一的區別是,在后者的定義體中有 yield 關鍵字。有些人認為定義生成器函數應該使用一個新的關鍵字,例如 gen,而不該使用 def。
舉個🌰 下面一個特別簡單的函數說明生成器的行為:
1 def gen_123(): 2 yield 1 3 yield 2 4 yield 3 5 6 7 print('gen_123是個函數', gen_123) 8 print('gen_123調用的時候是個生成器', gen_123()) 9 10 for i in gen_123(): 11 print(i) 12 13 g = gen_123() 14 print('使用next來一發', next(g)) 15 print('使用next來二發', next(g)) 16 print('使用next來三發', next(g)) 17 try: 18 next(g) 19 except StopIteration: 20 print('報錯哦,迭代器結束咯~') 21 else: 22 print('啊啊啊啊啊~~官人,好疼呀!')
以上代碼執行的結果為:
gen_123是個函數 <function gen_123 at 0x100562e18> gen_123調用的時候是個生成器 <generator object gen_123 at 0x102146308> 1 2 3 使用next來一發 1 使用next來二發 2 使用next來三發 3 報錯哦,迭代器里面沒有值了~
生成器函數會創建一個生成器對象,包裝生成器函數的定義體。把生成器傳給 next(...) 函數時,生成器函數會向前,執行函數定義體中的下一個 yield 語句,返回產出的值,並在函數定義體的當前位置暫停。最終,函數的定義體返回時,外層的生成器對象會拋出StopIteration 異常——這一點與迭代器協議一致。
1 def gen_AB(): #定義一個生成器,為毛線是生成器,因為有yield呀 2 print('Start') 3 yield 'A' #for循環第一次隱式調用next(gen_AB())函數時候,會打印Start,然后停止在第一個yield上產出 4 print('Continue') 5 yield 'B' #for循環第二次隱式調用next...產出B 6 print('End.') 7 8 9 for i in gen_AB(): 10 print('--->', i) #每次隱式調用的yield的值
以上代碼執行的結果:
Start ---> A Continue ---> B End.
Sentence類第4版:惰性實現
設計 Iterator 接口時考慮到了惰性:next(my_iterator) 一次生成一個元素。懶惰的反義詞是急迫,其實,惰性求值(lazy evaluation)和及早求值(eager evaluation)是編程語言理論方面的技術術語。
目前實現的幾版 Sentence 類都不具有惰性,因為 __init__ 方法急迫地構建好了文本中的單詞列表,然后將其綁定到 self.words 屬性上。這樣就得處理整個文本,列表使用的內存量可能與文本本身一樣多(或許更多,這取決於文本中有多少非單詞字符)。如果只需迭代前幾個單詞,大多數工作都是白費力氣。
sentence_gen2.py: 在生成器函數中調用 re.finditer生成器函數,實現 Sentence 類
1 class Sentence: 2 3 def __init__(self, text): 4 self.text = text 5 self.words = RE_WORD.finditer(self.text) #惰性的,省內存哦~ 生成器哦 6 7 def __repr__(self): 8 return 'Sentence(%s)' % reprlib.repr(self.text) 9 10 def __iter__(self): 11 for match in self.words: 12 yield match.group() #產出數據 13 return
Sentence類第5版:生成器表達式
簡單的生成器函數,如前面的 Sentence 類中使用的那個,可以替換成生成器表達式。
生成器表達式可以理解為列表推導的惰性版本:不會迫切地構建列表,而是返回一個生成器,按需惰性生成元素。也就是說,如果列表推導是制造列表的工廠,那么生成器表達式就是制造生成器的工廠。
先在列表推導中使用 gen_AB 生成器函數,然后在生成器表達式中使用
>>> def gen_AB(): #生成器 ... print('start') ... yield 'A' ... print('continue') ... yield 'B' ... print('end.') ... >>> res1 = [x*3 for x in gen_AB()] #列表推倒式 start continue end. >>> for i in res1: ... print('--->', i) #隱式調用,獲取yield的產出的值 ... ---> AAA ---> BBB >>> res2 = (x*3 for x in gen_AB()) #直接使用生成器 >>> res2 <generator object <genexpr> at 0x102ad25c8> >>> for i in res2: #循環輸出生成器的結果 ... print('--->', i) ... start ---> AAA continue ---> BBB end.
sentence_genexp.py:使用生成器表達式實現 Sentence類
1 import re 2 import reprlib 3 4 RE_WORD = re.compile(r'\w+') 5 6 7 class Sentence: 8 9 def __init__(self, text): 10 self.text = text 11 12 def __repr__(self): 13 return 'Sentence(%s)' % reprlib.repr(self.text) 14 15 def __iter__(self): 16 return (match.group() for match in RE_WORD.finditer(self.text))
這里不是生成器函數了(沒有 yield),而是使用生成器表達式構建生成器,然后將其返回。不過,最終的效果一樣:調用 __iter__ 方法會得到一個生成器對象。
何時使用生成器表達式
生成器表達式是創建生成器的簡潔句法,這樣無需先定義函數再調用。不過,生成器函數靈活得多,可以使用多個語句實現復雜的邏輯,也可以作為協程使用,遇到簡單的情況時,可以使用生成器表達式,因為這樣掃一眼就知道代碼的作用。
另一個示例:等差數列生成器
典型的迭代器模式作用很簡單——遍歷數據結構。不過,即便不是從集合中獲取元素,而是獲取序列中即時生成的下一個值時,也用得到這種基於方法的標准接口。例如,內置的 range 函數用於生成有窮整數等差數列(Arithmetic Progression,AP),itertools.count 函數用於生成無窮等差數列。
下面我們在控制台中對稍后實現的 ArithmeticProgression 類做一些測試,如 🌰 所示。這里,構造方法的簽名是ArithmeticProgression(begin, step[, end])。range() 函數與這個 ArithmeticProgression 類的作用類似,不過簽名是range(start, stop[, step])。我選擇使用不同的簽名是因為,創建等差數列時必須指定公差(step),而末項(end)是可選的。我還把參數的名稱由 start/stop 改成了 begin/end,以明確表明簽名不同。在示例 🌰 里的每個測試中,我都調用了 list() 函數,用於查看生成的值。
🌰 ArithmeticProgression 類
1 class ArithmeticProgression: 2 3 def __init__(self, begin, step, end=None): #__init__ 方法需要兩個參數:begin和step。end是可選的,如果值是None,那么生成的是無窮數列 4 self.begin = begin 5 self.step = step 6 self.end = end #None -> 無窮數列 7 8 def __iter__(self): 9 result = type(self.begin + self.step)(self.begin) #強制轉換成前面的類型,並把self.begin的值賦給result 10 forver = self.end is None 11 index = 0 12 while forver or result < self.end: #如果forver是None就一直循環下去或者當result得結果大於傳遞進來的end的值 13 yield result #產出result的值 14 index += 1 15 result = self.begin + self.step * index
演示 ArithmeticProgression 類的用法
>>> ap = ArithmeticProgression(0, 1, 3) >>> list(ap) [0, 1, 2] >>> ap = ArithmeticProgression(1, .5, 3) >>> list(ap) [1.0, 1.5, 2.0, 2.5] >>> ap = ArithmeticProgression(0, 1/3, 1) >>> list(ap) [0.0, 0.3333333333333333, 0.6666666666666666] >>> from fractions import Fraction >>> ap = ArithmeticProgression(0, Fraction(1, 3), 1) >>> list(ap) [Fraction(0, 1), Fraction(1, 3), Fraction(2, 3)] >>> from decimal import Decimal >>> ap = ArithmeticProgression(0, Decimal('.1'), .3) >>> list(ap) [Decimal('0.0'), Decimal('0.1'), Decimal('0.2')]
注意,在得到的等差數列中,數字的類型與 begin 或 step 的類型一致。如果需要,會根據 Python 算術運算的規則強制轉換類型
aritprog_gen 生成器函數
1 def aritprog_gen(begin, step, end=None): 2 result = type(begin + step)(begin) 3 forever = end is None 4 index = 0 5 while forever or result < end: 6 yield result 7 index += 1 8 result = begin + step * index
使用itertools模塊生成等差數列
Python 3.4 中的 itertools 模塊提供了 19 個生成器函數,結合起來使用能實現很多有趣的用法
例如,itertools.count 函數返回的生成器能生成多個數。如果不傳入參數,itertools.count 函數會生成從零開始的整數數列。不過,我們可以提供可選的 start 和 step 值,這樣實現的作用與aritprog_gen 函數十分相似:
>>> import itertools >>> gen = itertools.count(1, .5) >>> next(gen) 1 >>> next(gen) 1.5 >>> next(gen) 2.0 >>> next(gen) 2.5
然而,itertools.count 函數從不停止,因此,如果調用list(count()),Python 會創建一個特別大的列表,超出可用內存,在調用失敗之前,電腦會瘋狂地運轉。
不過,itertools.takewhile 函數則不同,它會生成一個使用另一個生成器的生成器,在指定的條件計算結果為 False 時停止。因此,可以把這兩個函數結合在一起使用,編寫下述代碼:
>>> gen = itertools.takewhile(lambda n:n<3, itertools.count(1, .5)) >>> list(gen) [1, 1.5, 2.0, 2.5]
aritprog_v3.py:與前面的 aritprog_gen 函數作用相同
1 import itertools 2 3 4 def aritprog_gen(begin, step, end=None): 5 first = type(begin + step)(begin) 6 ap_gen = itertools.count(first, step) 7 if end is not None: 8 ap_gen = itertools.takewhile(lambda n: n < end, ap_gen) 9 return ap_gen
標准庫中的生成器函數
標准庫提供了很多生成器,有用於逐行迭代純文本文件的對象,還有出色的 os.walk 函(https://docs.python.org/3/library/os.html#os.walk)。這個函數在遍歷目錄樹的過程中產出文件名,因此遞歸搜索文件系統像for 循環那樣簡單。
第一組是用於過濾的生成器函數:從輸入的可迭代對象中產出元素的子集,而且不修改元素本身。與 takewhile 函數一樣。大多數函數都接受一個斷言參數(predicate)。這個參數是個布爾函數,有一個參數,會應用到輸入中的每個元素上,用於判斷元素是否包含在輸出中。
用於過濾的生成器函數
模塊 |
函數 |
說明 |
itertools |
compress(it, selector_it) |
並行處理兩個可迭代的對象;如果selector_it中的元素是真值,產出it中對應的元素 |
itertools |
dropwhile(predicate, it) |
處理 it,跳過predicate的計算結果為真值得元素,然后產出剩下的各個元素(不在進行檢查) |
(內置) |
filter(predicate,it) |
把 it 中的各個元素傳給predicate,如果predicate(item) 返回真值,那么產出對應的元素;如果predicate是None,那么只產出真值元素 |
itertools |
filterfalse(predicate, it) |
與 filter 函數的作用類似,不過 predicate 的邏輯是相反的; predicate 返回假值時產出對應的元素 |
itertools |
islice(it, stop) 或 islice(it , start , stop , step=1) |
產出 it 的切片,作用類似於s[:stop]或s[satrt:stop:step],不過 it 可以任何可迭代的對象,而且這個函數實現的是惰性操作 |
itertools |
takewhile(predicate, it) |
predicate 返回真值時產出對象的元素,然后立即停止,不在進行檢查 |
演示用於過濾的生成器函數
>>> def vowel(c): ... return c.lower() in 'aeiou' ... >>> list(filter(vowel, 'Aardvark')) ['A', 'a', 'a'] >>> import itertools >>> list(itertools.filterfalse(vowel, 'Aardvark')) ['r', 'd', 'v', 'r', 'k'] >>> list(itertools.dropwhile(vowel, 'Aardvark')) ['r', 'd', 'v', 'a', 'r', 'k'] >>> list(itertools.takewhile(vowel, 'Aardvark')) ['A', 'a'] >>> list(itertools.compress('Aardvark', (1,0,1,1,0,1))) ['A', 'r', 'd', 'a'] >>> list(itertools.islice('Aardvark', 4)) ['A', 'a', 'r', 'd'] >>> list(itertools.islice('Aardvark', 4, 7)) ['v', 'a', 'r'] >>> list(itertools.islice('Aardvark', 1, 7, 2)) ['a', 'd', 'a']
下一組是用於映射的生成器函數:在輸入的單個可迭代對象(map 和starmap 函數處理多個可迭代的對象)中的各個元素上做計算,然后返回結果。 表中的生成器函數會從輸入的可迭代對象中的各個元素中產出一個元素。如果輸入來自多個可迭代的對象,第一個可迭代的對象到頭后就停止輸出。
用於映射的生成器函數
模塊 |
函數 |
說明 |
itertools |
accumulate(it, [func]) |
產出累積的總和;如果提供了 func,那么吧前面元素傳給它,然后把計算結果和下一個元素傳給它,以此類推,最后產出結果 |
(內置) |
enumerate(iterable, start=0) |
產出由兩個元素組成的元組,結構是(index, item),其中index和 start 開始計數,item則從 iterable 中獲取 |
(內置) |
map(func, it1, [it2, ...,itN] |
把 it 中的哥哥元素傳給fun,產出結果,如果傳入N個可迭代的對象,那么 func 必須能夠接受N個參數,並且要並行處理各個可迭代的對象 |
itertools |
starmap(func, it) |
把 it 中的哥哥元素傳給func,產出結果;輸入的課迭代對象應該產出可迭代的元素iit, 然后以func(*iit) 這種形式調用func |
演示 itertools.accumulate 生成器函數
>>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1] >>> import itertools >>> list(itertools.accumulate(sample)) # 計算總和 [5, 9, 11, 19, 26, 32, 35, 35, 44, 45] 12 12 >>> list(itertools.accumulate(sample, min)) # 計算最小值,因為傳遞了函數,所以兩個數對比,把最新的取到和后一個一起在傳遞給函數 [5, 4, 2, 2, 2, 2, 2, 0, 0, 0] >>> list(itertools.accumulate(sample, max)) # 計算最大值,原理同上 [5, 5, 5, 8, 8, 8, 8, 8, 9, 9] >>> import operator >>> list(itertools.accumulate(sample, operator.mul)) # 計算乘積 [5, 20, 40, 320, 2240, 13440, 40320, 0, 0, 0] >>> list(itertools.accumulate(range(1, 11), operator.mul)) [1, 2, 6, 24, 120, 720, 5040, 40320, 362880, 3628800] # 計算1!到10!階乘
演示用於映射的生成器函數
>>> list(enumerate('albatroz', 1)) # enumerate如果不給最后一位,會從0開始,給了數字會從數字開始,這里就是從1開始 [(1, 'a'), (2, 'l'), (3, 'b'), (4, 'a'), (5, 't'), (6, 'r'), (7, 'o'), (8, 'z')] >>> import operator >>> list(map(operator.mul, range(11), range(11))) # 計算各個整數的平方 [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100] >>> list(map(operator.mul, range(11), [2, 4, 8])) # 計算兩個可迭代對象中對應位置上的兩個元素之積,元素最少的那個可迭代對象到頭后就停止 [0, 4, 16] >>> list(map(lambda a, b: (a, b), range(11), [2, 4, 8])) # 等同於zip的用法 [(0, 2), (1, 4), (2, 8)] >>> import itertools >>> list(itertools.starmap(operator.mul, enumerate('albatroz', 1))) # 從1開始,根據字母所在的位置,吧字母重復相應的次數 ['a', 'll', 'bbb', 'aaaa', 'ttttt', 'rrrrrr', 'ooooooo', 'zzzzzzzz'] >>> sample = [5, 4, 2, 8, 7, 6, 3, 0, 9, 1] >>> list(itertools.starmap(lambda a, b: b/a, ... enumerate(itertools.accumulate(sample), 1))) # 計算平均值 [5.0, 4.5, 3.6666666666666665, 4.75, 5.2, 5.333333333333333, 5.0, 4.375, 4.888888888888889, 4.5]
接下來這一組是用於合並的生成器函數,這些函數都從輸入的多個可迭代對象中產出元素。chain 和 chain.from_iterable 按順序(一個接一個)處理輸入的可迭代對象,而 product、zip 和 zip_longest 並行處理輸入的各個可迭代對象。
合並多個可迭代對象的生成器函數
模塊 |
函數 |
說明 |
itertools |
chain(it1,.....,itN) |
先產出 it1 中的所有元素,然后產出 it2 中的所有元素,以此類推,無縫連接到一起 |
itertools |
chain.from_iterable(it) |
產出 it 生成的各個可迭代對象中的元素,一個接一個,無縫連接在一起;it 應該產出可迭代的元素,例如可迭代的對象列表 |
itertools |
product(it1, ..., itN, repeat=1) |
計算笛卡兒積:從輸入的各個可迭代對象中獲取元素,合並成由 N 個元素組成的元祖,與嵌套的 for 循環的效果一樣,repeat知名重復處理多少次輸入的可迭代對象 |
(內置) |
zip(it1, ..., itN) |
並行從輸入的各個可迭代對象中獲取元素,產出由 N 個元素組成的元祖,只要有一個可迭代的對象到頭了,就默默地停止 |
itertools |
zip_longest(it1, ...,itN, fillvalue=None) |
並行從輸入的各個可迭代對象中獲取元素,產出由 N 個元素組成的元組,等到最長的可迭代對象到頭后才停止,空缺的值使用 fillvalue 填充 |
展示 itertools.chain 和 zip 生成器函數及其同胞的用法。再次提醒,zip 函數的名稱出自 zip fastener 或 zipper(拉鏈,與ZIP 壓縮沒有關系)。“出色的 zip 函數”附注欄介紹過 zip 和itertools.zip_longest 函數。
演示用於合並的生成器函數
>>> list(itertools.chain('ABC', range(2))) # 調用chain函數時通常摻入兩個或更多個可迭代的對象 ['A', 'B', 'C', 0, 1] >>> list(itertools.chain(enumerate('ABC'))) # 如果只傳入一個可迭代的對象,那么chain函數並無卵用 [(0, 'A'), (1, 'B'), (2, 'C')] >>> list(itertools.chain.from_iterable(enumerate('ABC'))) # 但是chain.from_iterable函數從可迭代的對象中獲取每個元素,然后按順序把元素連接起來 [0, 'A', 1, 'B', 2, 'C'] >>> list(zip('ABC', range(5))) # zip常用於把兩個可迭代的對象合並成一系列由兩個元素組成的元祖 [('A', 0), ('B', 1), ('C', 2)] >>> list(zip('ABC', range(5), [10, 20, 30, 40])) # zip可以並行處理任意數量個可迭代的對象,不過只要有一個可迭代的對象到頭,生成器就停止 [('A', 0, 10), ('B', 1, 20), ('C', 2, 30)] >>> list(itertools.zip_longest('ABC', range(5))) # itertools.zip_logest函數的作用與zip類似,不過輸入的所有可迭代對象都會處理到頭,如果需要會填充None [('A', 0), ('B', 1), ('C', 2), (None, 3), (None, 4)] >>> list(itertools.zip_longest('ABC', range(5), fillvalue='?')) # fillvalue 關鍵字參數用於指定填充的值 [('A', 0), ('B', 1), ('C', 2), ('?', 3), ('?', 4)]
itertools.product 生成器是計算笛卡兒積的惰性方式;我們在多個 for 子句中使用列表推導計算過笛卡兒積。此外,也可以使用包含多個 for 子句的生成器表達式,以惰性方式計算笛卡兒積。
>>> list(itertools.product('ABC', range(2))) # 三個字符的字符串與兩個整數的值域得到的笛卡爾積是六個元祖 [('A', 0), ('A', 1), ('B', 0), ('B', 1), ('C', 0), ('C', 1)] >>> suits = 'spades hearts diamonds clubs'.split() >>> list(itertools.product('AK', suits)) # 兩張牌('AK')與四中花色得到的笛卡爾積是八個元祖 [('A', 'spades'), ('A', 'hearts'), ('A', 'diamonds'), ('A', 'clubs'), ('K', 'spades'), ('K', 'hearts'), ('K', 'diamonds'), ('K', 'clubs')] >>> list(itertools.product('ABC')) # 如果傳入一個可迭代的對象,product函數產出的是一系列只有一個元素的元祖,並沒有什么卵用~ [('A',), ('B',), ('C',)] >>> list(itertools.product('ABC', repeat=2)) # repeat=N 關鍵字參數告訴 product 函數重復 N 次處理輸入的各個可迭代對象 [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')] >>> list(itertools.product(range(2), repeat=3)) [(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1, 1, 1)] >>> rows = itertools.product('AB', range(2), repeat=2) >>> for row in rows: print(row) ... ('A', 0, 'A', 0) ('A', 0, 'A', 1) ('A', 0, 'B', 0) ('A', 0, 'B', 1) ('A', 1, 'A', 0) ('A', 1, 'A', 1) ('A', 1, 'B', 0) ('A', 1, 'B', 1) ('B', 0, 'A', 0) ('B', 0, 'A', 1) ('B', 0, 'B', 0) ('B', 0, 'B', 1) ('B', 1, 'A', 0) ('B', 1, 'A', 1) ('B', 1, 'B', 0) ('B', 1, 'B', 1)
生成器函數會從一個元素中產出多個值,擴展輸入的可迭代對象,如表
把輸入的各個元素擴展成多個輸出元素的生成器函數
模塊 |
函數 |
說明 |
itertools |
combinations(it, out_len) |
把 it 產出的 out_len 個元素組合在一起,然后產出 |
itertools |
combinations_with_replacement(it, out_len) |
把 it 產出的 out_len 個元素組合在一起,然后產出,包含相同元素的組合 |
itertools |
count(start=0, step=1) |
從 start 開始不斷產出數字,按 step 指定的步幅增加 |
itertools |
cycle(it) |
從 it 中產出各個元素,存儲各個元素的副本,然后按順序重復不斷產出各個元素 |
itertools |
permutations(it, out_len=None) |
把 out_len 個 it 產出的元素排列在一起,然后產出這些排列;out_len的默認值等於len(list(it)) |
itertools |
repeat(item, [times]) |
重復不斷地產出指定的元素,除非提供 times,指定次數 |
itertools 模塊中的 count 和 repeat 函數返回的生成器“無中生有”:這兩個函數都不接受可迭代的對象作為輸入。cycle 生成器會備份輸入的可迭代對象,然后重復產出對象中的元素
演示 count、repeat 和 cycle 的用法
>>> ct = itertools.count() # 使用count函數構建ct生成器 >>> next(ct) # 獲取ct的第一個元素 0 >>> next(ct), next(ct), next(ct) # 不能使用ct構建列表,因為ct是無窮的,所以我獲取了接下來的3個元素 (1, 2, 3) >>> list(itertools.islice(itertools.count(1, .3), 3)) # 如果使用 islice 或者 takewhile 函數做了限制,可以從count生成器中構建列表 [1, 1.3, 1.6] >>> cy = itertools.cycle('ABC') # 使用 'ABC' 構建一個cycle生成器,然后獲取第一個元素'A' >>> next(cy) 'A' >>> list(itertools.islice(cy, 7)) # 只有受到islice函數的限制,才能構建列表;這里獲取接下來的7個元素 ['B', 'C', 'A', 'B', 'C', 'A', 'B'] >>> rp = itertools.repeat(7) # 構建一個 repeat 生成器,始終產生數字 7 >>> next(rp), next(rp) (7, 7) >>> list(itertools.repeat(8, 4)) # 傳入 times 參數可以限制repeat 生成器生成的元素數量,這里會生成4次數字8 [8, 8, 8, 8] >>> list(map(operator.mul, range(11), itertools.repeat(5))) # ➒ repeat 函數最常見用途:為 map 函數提供固定參數,這里提供的是乘數 5 [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]
在 itertools 模塊的文檔中(https://docs.python.org/3/library/itertools.html),combinations、combinations_和 permutations 生成器函數,連同 product 函數,稱為組合學生成器(combinatoric generator)。itertools.product 函數和其余的組合學函數有緊密的聯系。
組合學生成器函數會從輸入的各個元素中產出多個值
>>> list(itertools.combinations('ABC', 2)) # 'ABC' 中每個元素(len()==2) 的各種組合,在生成的元素中元素的順序無關緊要(可以視為集合) [('A', 'B'), ('A', 'C'), ('B', 'C')] >>> list(itertools.combinations_with_replacement('ABC', 2)) # 'ABC'中每個元素(len()==2) 的各種組合,包括相同元素的組合 [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')] >>> list(itertools.permutations('ABC', 2)) # 'ABC' 中每兩個元素(len()==2)的各種排列;在生成的元祖中,元素的順序有重要意義 [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')] >>> list(itertools.product('ABC', repeat=2)) # 'ABC' 和 'ABC' (repeat=2 的效果)的笛卡爾積 [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'B'), ('B', 'C'), ('C', 'A'), ('C', 'B'), ('C', 'C')]
最后一組生成器函數用於產出輸入的可迭代對象中的全部元素,不過會以某種方式重新排列。其中有兩個函數會返回多個生成器,分別是 itertools.groupby 和 itertools.tee。這一組里的另一個生成器函數,內置的 reversed 函數,是所述的函數中唯一一個不接受可迭代的對象,而只接受序列為參數的函數。這在情理之中,因為reversed 函數從后向前產出元素,而只有序列的長度已知時才能工作。不過,這個函數會按需產出各個元素,因此無需創建反轉的副本。
用於重新排列元素的生成器函數
模塊 |
函數 |
說明 |
itertools |
groupby(it,key=None) |
產出由兩個元素組成的元素,形式為 (key,group),其中 key 是分組標准, group 是生成器,用於產出分組里的元素 |
(內置) |
reversed(seq) |
從后向前,倒序產出 seq 中的元素;seq 必須是序列,或者是實現了__reversed__特殊方法的對象 |
itertools |
tee(it, n=2) |
產出一個由 n 個生成器組成的元祖,每個生成器用於單獨產出輸入的可迭代對象中的元素 |
演示 itertools.groupby 函數和內置的 reversed 函數的用法。注意,itertools.groupby 假定輸入的可迭代對象要使用分組標准排序;即使不排序,至少也要使用指定的標准分組各個元素。
itertools.groupby 函數的用法
>>> list(itertools.groupby('LLLLAAGGG')) # ➊ groupby函數產出(key, group_generator)這種形式的元祖 [('L', <itertools._grouper object at 0x102227cc0>), ('A', <itertools._grouper object at 0x102227b38>), ('G', <itertools._grouper object at 0x102227b70>)] >>> for char, group in itertools.groupby('LLLLAAAGG'): # 處理 groupby 函數返回的生成器要嵌套迭代;這里在外城使用 for 循環,內層使用列表推導式 ... print(char, '->', list(group)) ... L -> ['L', 'L', 'L', 'L'] A -> ['A', 'A',] G -> ['G', 'G', 'G'] >>> animals = ['duck', 'eagle', 'rat', 'giraffe', 'bear', ... 'bat', 'dolphin', 'shark', 'lion'] >>> animals.sort(key=len) # 為了使用groupby函數,要排序輸入;這里按照單詞的長度排序 >>> animals ['rat', 'bat', 'duck', 'bear', 'lion', 'eagle', 'shark', 'giraffe', 'dolphin'] >>> for length, group in itertools.groupby(animals, len): # 再次遍歷 key 和 group 值對,把 key 顯示出來,並把 group 擴展成列表 ... print(length, '->', list(group)) ... 3 -> ['rat', 'bat'] 4 -> ['duck', 'bear', 'lion'] 5 -> ['eagle', 'shark'] 7 -> ['giraffe', 'dolphin'] >>> for length, group in itertools.groupby(reversed(animals), len): # 這里使用 reverse 生成器從右向左迭代 animals ... print(length, '->', list(group)) ... 7 -> ['dolphin', 'giraffe'] 5 -> ['shark', 'eagle'] 4 -> ['lion', 'bear', 'duck'] 3 -> ['bat', 'rat']
這一組里的最后一個生成器函數是 iterator.tee,這個函數只有一個作用:從輸入的一個可迭代對象中產出多個生成器,每個生成器都可以產出輸入的各個元素。
>>> list(itertools.tee('ABC')) [<itertools._tee object at 0x10222abc8>, <itertools._tee object at 0x10222ac08>] >>> g1, g2 = itertools.tee('ABC') >>> next(g1) 'A' >>> next(g2) 'A' >>> next(g2) 'B' >>> list(g1) ['B', 'C'] >>> list(g2) ['C'] >>> list(zip(*itertools.tee('ABC'))) [('A', 'A'), ('B', 'B'), ('C', 'C')]
Python 3.3中新出現的句法:yieldfrom
如果生成器函數需要產出另一個生成器生成的值,傳統的解決方法是使用嵌套的 for 循環。
例如,下面是我們自己實現的 chain 生成器:
>>> def chain(*iterable): ... for it in iterable: ... for i in it: ... yield i ... >>> s = 'ABC' >>> t = tuple(range(3)) >>> list(chain(s, t)) ['A', 'B', 'C', 0, 1, 2]
chain 生成器函數把操作依次交給接收到的各個可迭代對象處理。為此,“PEP 380 — Syntax for Delegating to a Subgenerator”引入了一個新句法,如下述控制台中的代碼清單所示:
1 def chain(*iterable): 2 for i in iterable: 3 yield from i 4 5 s = 'ABC' 6 t = tuple(range(3)) 7 8 print(list(chain(s, t)))
可以看出,yield from i 完全代替了內層的 for 循環。在這個示例中使用 yield from 是對的,而且代碼讀起來更順暢,不過感覺更像是語法糖。除了代替循環之外,yield from 還會創建通道,把內層生成器直接與外層生成器的客戶端聯系起來。把生成器當成協程使用時,這個通道特別重要,不僅能為客戶端代碼生成值,還能使用客戶端代碼提供的值。
可迭代的歸約函數
表中的函數都接受一個可迭代的對象,然后返回單個結果。這些函數叫“歸約”函數、“合攏”函數或“累加”函數。其實,這里列出的每個內置函數都可以使用 functools.reduce 函數實現,內置是因為使用它們便於解決常見的問題。此外,對 all 和 any 函數來說,有一項重要的優化措施是 reduce 函數做不到的:這兩個函數會短路(即一旦確定了結果就立即停止使用迭代器)
讀取迭代器,返回單個值的內置函數
模塊 |
函數 |
說明 |
(內置) |
all(it) |
it 中的所有元素都為真時返回True,否則就返回False;all([])返回True |
(內置) |
any(it) |
只要 it 中有元素為真值就返回 True,否則返回Fasle;all([])返回Fasle |
(內置) |
max(it, [key=,] [default=]) |
返回 it 中值最大的元素,key是排序函數,與 sorted 函數中的一樣;如果可迭代的對象為空,返回default |
(內置) |
min(it, [key=,] [default=]) |
返回 it 中值最小的元素, key 是排序函數,與sorted 函數中的一樣;如果可迭代的對象為空,返回default |
functools |
reduce(func, it, [initial]) |
把前兩個元素傳給 func,然后把計算結果和第三個元素傳給func,以此類推,返回最后的結果;如果提供inital,把它當做第一個元素傳入 |
(內置) |
sum(it, start=0) |
it 中所有元素的中和,如果提供可選的 start,會把它加上(計算浮點數的加法時,可以用math.fsum函數提高精度) |
all 和 any 函數的操作演示如 🌰 所示
>>> all([1, 2, 3]) True >>> all([1, 0, 3]) False >>> all([]) True >>> any([1, 2, 3]) True >>> any([1, 0, 3]) True >>> any([0, 0.0]) False >>> any([]) False >>> g = (n for n in [0, 0.0, 7, 8]) >>> any(g) True >>> next(g) 8
深入分析iter函數
如前所述,在 Python 中迭代對象 x 時會調用 iter(x)。可是,iter 函數還有一個鮮為人知的用法:傳入兩個參數,使用常規的函數或任何可調用的對象創建迭代器。這樣使用時,第一個參數必須是可調用的對象,用於不斷調用(沒有參數),產出各個值;第二個值是哨符,這是個標記值,當可調用的對象返回這個值時,觸發迭代器拋出 StopIteration 異常,而不產出哨符。
🌰 展示如何使用 iter 函數擲骰子,直到擲出 1 點為止:
>>> def d6(): ... return randint(1, 6) ... >>> d6_iter = iter(d6, 1) >>> d6_iter <callable_iterator object at 0x00000000029BE6A0> >>> for roll in d6_iter: ... print(roll) ... 4 3 6 3
注意,這里的 iter 函數返回一個 callable_iterator 對象。示例中的 for 循環可能運行特別長的時間,不過肯定不會打印 1,因為 1 是哨符。與常規的迭代器一樣,這個示例中的 d6_iter 對象一旦耗盡就沒用了。如果想重新開始,必須再次調用 iter(...),重新構建迭代器。
另外一個讀取文件的 🌰 讀取文件直到遇到空行或者到達文件末尾為止:
import os with open('/tmp/bashrc') as f: for line in iter(f.readline, os.linesep): print(line)