测试开发之Python核心笔记(15):迭代器与生成器
15.1 可迭代對象Iterable
還記得for循環(huán)嗎?for循環(huán)可以循環(huán)迭代處理字符串以及列表、元組、字典、集合這些容器類型,知道為什么這些數(shù)據(jù)類型可以被for迭代嗎?因為這些對象都是可迭代對象。
判斷是否是可迭代對象,可以isinstance(obj, Iterable)判斷,輸出True表示obj對象是可迭代的(iterable)。
15.2 迭代器iterator
通過迭代器,程序員可以迭代非序列類型,就是除了列表、元素、字典和集合之外類型。
迭代器通過next()方法獲取對象中的下一個元素,可以把它看做一個數(shù)據(jù)流,我們不知道他的長度,一邊使用一邊計算下一個數(shù)據(jù),是一種惰性計算。迭代器對象從集合的第一個元素開始訪問,直到所有的元素被訪問完結束。
迭代器的主要優(yōu)點是節(jié)約內存(循環(huán)過程中,數(shù)據(jù)不用一次讀入,在處理文件對象時特別有用,因為文件也是迭代器對象)、不依賴索引取值、實現(xiàn)惰性計算(需要時再取值計算);
with open('java.txt') as f:for line in f:print(line)這樣每次讀取一行處理一行,而不是一次性將整個文件讀入,節(jié)約內存。
迭代器有個缺點,就是只能遍歷一遍,不能遍歷第二遍。因為遍歷訪問時,只能向前一個一個訪問數(shù)據(jù),直到訪問到沒有數(shù)據(jù)了,不能回頭。也就是說迭代器只能往前不能后退。
l = ['a', 'n', 's', 'd'] l_iterator = iter(l) # 通過iter函數(shù)得到一個迭代器 for i in l_iterator:print(i)for i in l_iterator: # 經過第一遍迭代后,迭代器中已經沒有數(shù)據(jù)了,或者說已經訪問到尾部了。print(i)15.2.1 通過 iter() 函數(shù)得到迭代器
tuple、list、dict、str雖然是Iterable,卻不是迭代器Iterator。可以通過 iter() 函數(shù)返回一個迭代器。通過 next() 方法就可以實現(xiàn)遍歷,調用next方法,要么得到這個容器的下一個對象,要么得到一個 StopIteration 的錯誤。Python的for循環(huán)迭代tuple、list本質上是先將他們轉成迭代器Iterator,然后通過不斷調用next()函數(shù)實現(xiàn)的。
l_iterator=iter([1,2,3,4,5])15.2.1 通過類實現(xiàn)迭代器
在Python中有很多通過類實現(xiàn)的迭代器,比如reversed(),enumerate(),通過查看源碼可以發(fā)現(xiàn),這些類都實現(xiàn)了__next__方法 和 __iter__ 方法。
如果你想讓一個類對象可以被迭代,那么把一個類實現(xiàn)成一個迭代器,就是讓類繼承于Iterable,然后重寫兩個方法:__next__方法 和 __iter__ 方法。
__iter__ 方法返回一個特殊的迭代器對象。
__next__ 會返回下一個迭代器對象。
例如,實現(xiàn)一個遞減迭代器,對某個正整數(shù),依次遞減 1,直到 0。
from collections.abc import Iterableclass Decrease(Iterable):def __init__(self, init):self.init = initdef __iter__(self): # 返回對象本身return selfdef __next__(self):while 0 < self.init:self.init -= 1return self.init # 返回下一個raise StopIteration # 通過 raise 終斷nextfor i in Decrease(6): # 可以用for循環(huán)迭代這個類對象了print(i)15.3 itertools 模塊
這個模塊實現(xiàn)了一系列快速、高效的迭代器。這些迭代器本身或組合都很有用。這一小節(jié)就來介紹一些非常好用的迭代器。
Help(itertools)可以看到內置的迭代器。如果使用Pycharm編輯器,按兩下Shift,輸入itertools,勾選上Include non-project items,在彈出的頁面上選擇itertools文件,就可以進入itertools.py文件。你會看到這樣內容,這里列出來了itertools模塊提供的所有迭代器:
""" Functional tools for creating and using iterators.Infinite iterators: count(start=0, step=1) --> start, start+step, start+2*step, ... cycle(p) --> p0, p1, ... plast, p0, p1, ... repeat(elem [,n]) --> elem, elem, elem, ... endlessly or up to n timesIterators terminating on the shortest input sequence: accumulate(p[, func]) --> p0, p0+p1, p0+p1+p2 chain(p, q, ...) --> p0, p1, ... plast, q0, q1, ... chain.from_iterable([p, q, ...]) --> p0, p1, ... plast, q0, q1, ... compress(data, selectors) --> (d[0] if s[0]), (d[1] if s[1]), ... dropwhile(pred, seq) --> seq[n], seq[n+1], starting when pred fails groupby(iterable[, keyfunc]) --> sub-iterators grouped by value of keyfunc(v) filterfalse(pred, seq) --> elements of seq where pred(elem) is False islice(seq, [start,] stop [, step]) --> elements fromseq[start:stop:step] starmap(fun, seq) --> fun(*seq[0]), fun(*seq[1]), ... tee(it, n=2) --> (it1, it2 , ... itn) splits one iterator into n takewhile(pred, seq) --> seq[0], seq[1], until pred fails zip_longest(p, q, ...) --> (p[0], q[0]), (p[1], q[1]), ... Combinatoric generators: product(p, q, ... [repeat=1]) --> cartesian product permutations(p[, r]) combinations(p, r) combinations_with_replacement(p, r) """下面我們重點學習其中的幾個迭代器。
15.3.1 拼接迭代器chain
可將多個可迭代對象變成為單個迭代器。在Pycharm的導航欄中,選擇Navigate——>File Structure,就可以列出itertools.py模塊中所有的類。
點擊chain類,可以看到chain的說明:
class chain(object):"""chain(*iterables) --> chain objectReturn a chain object whose .__next__() method returns elements from thefirst iterable until it is exhausted, then elements from the nextiterable, until all of the iterables are exhausted."""文檔的意思就是說,創(chuàng)建一個迭代器,它首先返回第一個可迭代對象中所有元素,接著返回下一個可迭代對象中所有元素,直到耗盡所有可迭代對象中的元素。可將多個可迭代對象變成為單個迭代器。舉幾個例子,在Pycharm中編輯:
from itertools import chainchain_iterator = chain("ABCDef", "1234") # 兩個可迭代對象:"ABCDef"和"1234" print(list(chain_iterator)) # ['A', 'B', 'C', 'D', 'e', 'f', '1', '2', '3', '4']使用 chain() 的一個常見場景是,當你想對不同的集合中所有元素執(zhí)行某些操作的時候。比如,你想對兩個列表的元素都計算平方和并形成一個新列表。可以這樣做:
from itertools import chaindef square_multi_iterables(*iterables): # 定義一個生成器chain_iterator = chain(*iterables) # 利用chain生成迭代器for l in chain_iterator: # 對迭代器里面的元素求進行平方yield l * l # yield 返回if __name__ == '__main__':lst1 = [1, 2, 3]lst2 = [4, 5, 6]for i in square_multi_iterables(lst1, lst2):print(i)print(list(square_multi_iterables(lst2, lst1)))這種解決方案要比使用兩個單獨的循環(huán)更加優(yōu)雅!
15.3.2 累積迭代器accumulate
先看看源碼中怎么說:
class accumulate(object):"""accumulate(iterable[, func]) --> accumulate objectReturn series of accumulated sums (or other binary function results)."""將可迭代對象應用到func函數(shù)上,返回可迭代對象的累積迭代器。如果func沒有提供,則返回可迭代對象累計和組成的迭代器。
看個例子說明一下:
from itertools import accumulatelst = [1, 2, 3, 4, 5, 6] for i in accumulate(lst):print(i) print(list(accumulate(lst))) # 返回 [1, 3, 6, 10, 15, 21]這個例子沒有提供func函數(shù),默認是對原序列累計求和。再來看看提供func函數(shù)的用法。
from itertools import accumulatelst = [1, 2, 3, 4, 5, 6] for i in accumulate(lst, lambda x, y: x * y):print(i) print(list(accumulate(lst, lambda x, y: x * y))) # 返回 [1, 2, 6, 24, 120, 720]15.3.3 排列組合迭代器
依然是先看看代碼怎么說:
class combinations(object):"""combinations(iterable, r) --> combinations objectReturn successive r-length combinations of elements in the iterable.combinations(range(4), 3) --> (0,1,2), (0,1,3), (0,2,3), (1,2,3)"""返回由輸入iterable可迭代對象中元素組成長度為 r 的子序列。組合按照字典序返回。所以如果輸入 iterable 是有序的,生成的組合元組也是有序的。
即使元素的值相同,不同位置的元素也被認為是不同的。如果元素各自不同,那么每個組合中沒有重復元素。
舉個例子看看,如何根據(jù) “ABCD”, 輸出 [‘AB’, ‘AC’, ‘AD’, ‘BC’, ‘BD’, ‘CD’]
def my_combinations(iterables, length):for i in combinations(iterables, length): yield "".join(i)if __name__ == '__main__':lst=[]for element in my_combinations("ABCD", 2): # 兩個元素組成的排列組合lst.append(element)print(lst) # ['AB', 'AC', 'AD', 'BC', 'BD', 'CD']15.3.3 壓縮迭代器compress
看看源碼怎么說:
class compress(object):"""compress(data, selectors) --> iterator over selected dataReturn data elements corresponding to true selector elements.Forms a shorter iterator from selected data elements using theselectors to choose the data elements."""創(chuàng)建一個迭代器,它返回 data 中經 selectors 真值測試為 True 的元素。迭代器在兩者較短的長度處停止。
print(list(compress('ABCDEF', [1, 0, 1, 0, 1, 1]))) # 輸出['A', 'C', 'E', 'F']相當于是下面的這段代碼:
def compress(data, selectors):return (d for d, s in zip(data, selectors) if s)15.3.4 丟棄迭代器dropwhile
看看源碼的描述:
class dropwhile(object):"""dropwhile(predicate, iterable) --> dropwhile objectDrop items from the iterable while predicate(item) is true.Afterwards, return every element until the iterable is exhausted."""創(chuàng)建一個迭代器,如果 predicate 為true,迭代器丟棄這些元素,然后返回其他元素。迭代器在 predicate 首次為false之前不會產生任何輸出,所以可能需要一定長度的啟動時間。
舉個例子:
from itertools import dropwhileprint(list(dropwhile(lambda x: x < 5, [1, 4, 6, 4, 1]))) # 輸出 [6, 4, 1]大致相當于:
def my_dropwhile(predicate, iterable):iterable = iter(iterable)for x in iterable:if not predicate(x):yield x # 返回第一個不滿足predicate的值后退出這個循環(huán)breakfor x in iterable: # 接著循環(huán)剩下的元素yield xprint(list(my_dropwhile(lambda x: x < 5, [1, 4, 6, 4, 1]))) # 輸出 [6, 4, 1]更多的迭代器,可以參考itertools.py源碼。
15.3 生成器generator(特殊的迭代器)
帶 yield 的函數(shù)是生成器,而生成器也是一種迭代器。所以,生成器也有上面那些迭代器的特點。
通俗理解 yield,可結合函數(shù)的返回值關鍵字 return,yield 是一種特殊的 return。說是特殊的 return,是因為執(zhí)行遇到 yield 時,立即返回,這是與 return 的相似之處。
不同之處在于:下次進入函數(shù)時直接到 yield 的下一個語句,而 return 后再進入函數(shù),還是從函數(shù)體的第一行代碼開始執(zhí)行。
聲明一個迭代器很簡單,通過列表解析式[i for i in range(100000000)]就可以生成一個包含一億個元素的列表。每個元素在生成后都會保存到內存中(這個過程很慢),而且占用了巨量的內存,內存不夠的話就會出現(xiàn) OOM 錯誤。不過,有的時候,我們并不需要提前在內存中保存這么多東西,比如對元素累加求和,我們只需要知道每個元素在相加的那一刻是多少就行了,用完就可以扔掉了。
生成器generator不會提前生成好所有的值放到內存中,只有調用next方法(生成器的方法)時,才會生成下一個變量,從而加快代碼執(zhí)行速度并節(jié)省內存。
得到生成器的方式有兩種:
15.3.1 小括號定義生成器
將[]換成(),就可以簡單地把列表解析式改成生成器generator,就得到一個生成器(i for i in range(100000000))。列表解析式最常用,但是盡量把列表解析式變成生成器,因為這樣節(jié)省內存,節(jié)省內存的思想應該處處體現(xiàn)在代碼里,這樣才能體現(xiàn)水平。
15.3.2 通過yield關鍵字定義生成器
def fib(max):n, a, b = 0, 0, 1while n < max:yield ba, b = b, a + bn = n + 1return 'done'在每次調用next()的時候執(zhí)行,遇到y(tǒng)ield語句暫停,返回yeild表達式后面的值,再次調用next時從yield語句處繼續(xù)執(zhí)行。
15.3.3 生成器的價值
生成器除了可以利用惰性計算來節(jié)約內存,還能提高代碼的可讀性。例如,求一段文本中,每個單詞的的起始下標。不用生成器的方案:
def index_words(text):result = [] # 存放下標if text:result.append(0)for index, letter in enumerate(text, 1): # 第二個參數(shù)是1,如果不寫是什么情況if letter == " ":result.append(index)return resultif __name__ == '__main__':enumerate_desc = "The enumerate object yields pairs containing a count (from start, which defaults to zero) and a value yielded by the iterable argument."print(index_words(enumerate_desc))如果使用生成器:
def index_words(text):if text:yield 0 # 第一次返回for index, letter in enumerate(text, 1): if letter == " ":yield index # 每次調用返回if __name__ == '__main__':enumerate_desc = "The enumerate object yields pairs containing a count (from start, which defaults to zero) and a value yielded by the iterable argument."for index in index_words(enumerate_desc):print(index)print(list(index_words(enumerate_desc)))可以使用生成器的代碼更加清晰,代碼行數(shù)更少。在不使用生成器時,對于每次結果,首先要執(zhí)行一個append操作,最終才返回result。而使用生成器時候,直接yield index,少了列表操作帶來的干擾,一眼就能看出,代碼是要返回index。
kafka消費數(shù)據(jù)可以用yield生成器。還有pytest的fixture函數(shù)中也用到了,還有啟動webdrvier也是用yield。
工作中一定要多多使用生成器。但是要注意,因為生成器也是迭代器,因此生成器只能遍歷一次。
15.4 練習題
index_generator 會返回一個 Generator 對象,需要使用 list 轉換為列表后,才能用 print 輸出。
LeetCode 鏈接如下:https://leetcode.com/problems/is-subsequence/ 。序列就是列表,子序列則指的是,一個列表的元素在第二個列表中都按順序出現(xiàn),但是并不必挨在一起。舉個例子,[1, 3, 5] 是 [1, 2, 3, 4, 5] 的子序列,[1, 4, 3] 則不是。
要解決這個問題,常規(guī)算法是貪心算法。維護兩個指針指向兩個列表的最開始,然后對第二個序列一路掃過去,如果某個數(shù)字和第一個指針指的一樣,那么就把第一個指針前進一步。第一個指針移出第一個序列最后一個元素的時候,返回 True,否則返回 False。
不過本小節(jié),使用生成器來實現(xiàn)。
def is_subsequence(a, b):b = iter(b)return all(i in b for i in a)print(is_subsequence([1, 3, 5], [1, 2, 3, 4, 5])) print(is_subsequence([1, 4, 3], [1, 2, 3, 4, 5]))########## 輸出 ##########True False總結
以上是生活随笔為你收集整理的测试开发之Python核心笔记(15):迭代器与生成器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一文总结买卖股票的最佳时机的所有情况(附
- 下一篇: JavaBean实例9:获取汉字的拼音简