摘要:例如,以下對(duì)兩個(gè)的相應(yīng)元素求和這個(gè)例子很好的解釋了如何構(gòu)建中所謂的迭代器代數(shù)的函數(shù)的含義。為簡(jiǎn)單起見,假設(shè)輸入的長度可被整除。接受兩個(gè)參數(shù)一個(gè)可迭代的正整數(shù)最終會(huì)在中個(gè)元素的所有組合的元組上產(chǎn)生一個(gè)迭代器。
前言
大家好,今天想和大家分享一下我的itertools學(xué)習(xí)體驗(yàn)及心得,itertools是一個(gè)Python的自帶庫,內(nèi)含多種非常實(shí)用的方法,我簡(jiǎn)單學(xué)習(xí)了一下,發(fā)現(xiàn)可以大大提升工作效率,在sf社區(qū)內(nèi)沒有發(fā)現(xiàn)十分詳細(xì)的介紹,因此希望想自己做一個(gè)學(xué)習(xí)總結(jié)。也和朋友們一起分享一下心得
首先,有關(guān)itertools的詳細(xì)介紹,我參考的是Python 3.7官方文檔:itertools — Functions creating iterators for efficient looping,大家感興趣可以去看看,目前還沒有中文版本,十分遺憾,這里不得不吐槽一句,為啥有日語,韓語,中文的版本沒有跟上呢?
書規(guī)正傳,itertools 我個(gè)人評(píng)價(jià)是Python3里最酷的東西! 如果你還沒有聽說過它,那么你就錯(cuò)過了Python 3標(biāo)準(zhǔn)庫的一個(gè)最大隱藏寶藏,是的,我很快就拋棄了剛剛分享的collections模塊:Python 進(jìn)階之路 (七) 隱藏的神奇寶藏:探秘Collections,畢竟男人都是大豬蹄子
網(wǎng)上有很多優(yōu)秀的資源可用于學(xué)習(xí)itertools模塊中的功能。但對(duì)我而言,官方文檔本身總是一個(gè)很好的起點(diǎn)。學(xué)會(huì)做甜點(diǎn)之前,總是要會(huì)最基礎(chǔ)的面包。這篇文章便是基本基于文檔歸納整理而來。
我在學(xué)習(xí)后的整體感受是,關(guān)于itertools只知道它包含的函數(shù)是遠(yuǎn)遠(yuǎn)不夠的。真正的強(qiáng)大之處在于組合這些功能以創(chuàng)建快速,占用內(nèi)存效率極少,漂亮優(yōu)雅的代碼。
在這篇很長的文章里,我會(huì)全面復(fù)盤我的學(xué)習(xí)歷程,爭(zhēng)取全面復(fù)制每一個(gè)細(xì)節(jié),在開始之前,如果朋友們還不太知道迭代器和生成器是什么,可以參考以下科普掃盲:
菜鳥教程 迭代器生成器
廖雪峰的迭代器講解
廖雪峰的生成器講解
神奇的itertools好啦,坐好扶穩(wěn),我們準(zhǔn)備上車了,根據(jù)官方文檔的定義:
This module implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML. Each has been recast in a form suitable for Python.
翻譯過來大概就是它是一個(gè)實(shí)現(xiàn)了許多迭代器構(gòu)建的模塊,它們受到來自APL,Haskell和SML的構(gòu)造的啟發(fā)......可以提高效率啥的,
這主要意味著itertools中的函數(shù)是在迭代器上“操作”以產(chǎn)生更復(fù)雜的迭代器。
例如,考慮內(nèi)置的zip()函數(shù),該函數(shù)將任意數(shù)量的iterables作為參數(shù),并在其相應(yīng)元素的元組上返回迭代器:
print(list(zip([1, 2, 3], ["a", "b", "c"]))) Out:[(1, "a"), (2, "b"), (3, "c")]
這里讓我們思考一個(gè)問題,這個(gè)zip函數(shù)到底是如何工作的?
與所有其他list一樣,[1,2,3] 和 ["a","b","c"] 是可迭代的,這意味著它們可以一次返回一個(gè)元素。
從技術(shù)上講,任何實(shí)現(xiàn):
.__ iter __()
或.__ getitem __()
方法的Python對(duì)象都是可迭代的。如果對(duì)這方面有疑問,大家可以看前言部分提到的教程哈
其實(shí)有關(guān)iter()這個(gè)內(nèi)置函數(shù),當(dāng)在一個(gè)list或其他可迭代的對(duì)象 x 上調(diào)用時(shí),會(huì)返回x自己的迭代器對(duì)象:
iter([1, 2, 3, 4]) iter((1,2,3,4)) iter({"a":1,"b":2}) Out:
實(shí)際上,zip()函數(shù)通過在每個(gè)參數(shù)上調(diào)用iter(),然后使用next()推進(jìn)iter()返回的每個(gè)迭代器并將結(jié)果聚合為元組來實(shí)現(xiàn)。 zip()返回的迭代器遍歷這些元組
而寫到這里不得不回憶一下,之前在 Python 進(jìn)階之路 (五) map, filter, reduce, zip 一網(wǎng)打盡我給大家介紹的神器map()內(nèi)置函數(shù),其實(shí)某種意義上也是一個(gè)迭代器的操作符而已,它以最簡(jiǎn)單的形式將單參數(shù)函數(shù)一次應(yīng)用于可迭代的sequence的每個(gè)元素:
模板: map(func,sequence)
list(map(len, ["xiaobai", "at", "paris"])) Out: [7, 2, 5]
參考map模板,不難發(fā)現(xiàn):map()函數(shù)通過在sequence上調(diào)用iter(),使用next()推進(jìn)此迭代器直到迭代器耗盡,并將func 應(yīng)用于每步中next()返回的值。在上面的例子里,在["xiaobai", "at", "paris"]的每個(gè)元素上調(diào)用len(),從而返回一個(gè)迭代器包含list中每個(gè)元素的長度
由于迭代器是可迭代的,因此可以用 zip()和 map()在多個(gè)可迭代中的元素組合上生成迭代器。
例如,以下對(duì)兩個(gè)list的相應(yīng)元素求和:
a = [1, 2, 3] b = [4, 5, 6] list(map(sum, zip(a,b))) Out: [5, 7, 9]
這個(gè)例子很好的解釋了如何構(gòu)建itertools中所謂的 “迭代器代數(shù)” 的函數(shù)的含義。我們可以把itertools視為一組構(gòu)建磚塊,可以組合起來形成專門的“數(shù)據(jù)管道”,就像這個(gè)求和的例子一樣。
其實(shí)在Python 3里,如果我們用過了map() 和 zip() ,就已經(jīng)用過了itertools,因?yàn)檫@兩個(gè)函數(shù)返回的就是迭代器!
我們使用這種 itertools 里面所謂的 “迭代器代數(shù)” 帶來的好處有兩個(gè):
提高內(nèi)存效率 (lazy evaluation)
提速
可能有朋友對(duì)這兩個(gè)好處有所疑問,不要著急,我們可以分析一個(gè)具體的場(chǎng)景:
現(xiàn)在我們有一個(gè)list和正整數(shù)n,編寫一個(gè)將list 拆分為長度為n的組的函數(shù)。為簡(jiǎn)單起見,假設(shè)輸入list的長度可被n整除。例如,如果輸入= [1,2,3,4,5,6] 和 n = 2,則函數(shù)應(yīng)返回 [(1,2),(3,4),(5,6)]。
我們首先想到的解決方案可能如下:
def naive_grouper(lst, n): num_groups = len(lst) // n return [tuple(lst[i*n:(i+1)*n]) for i in range(num_groups)]
我們進(jìn)行簡(jiǎn)單的測(cè)試,結(jié)果正確:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] naive_grouper(nums, 2) Out: [(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
但是問題來了,如果我們?cè)噲D傳遞一個(gè)包含1億個(gè)元素的list時(shí)會(huì)發(fā)生什么?我們需要大量?jī)?nèi)存!即使有足夠的內(nèi)存,程序也會(huì)掛起一段時(shí)間,直到最后生成結(jié)果
這個(gè)時(shí)候如果我們使用itertools里面的迭代器就可以大大改善這種情況:
def better_grouper(lst, n): iters = [iter(lst)] * n return zip(*iters)
這個(gè)方法中蘊(yùn)含的信息量有點(diǎn)大,我們現(xiàn)在拆開一個(gè)個(gè)看,表達(dá)式 [iters(lst)] * n 創(chuàng)建了對(duì)同一迭代器的n個(gè)引用的list:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] iters = [iter(nums)] * 2 list(id(itr) for itr in iters) # Id 沒有變化,就是創(chuàng)建了n個(gè)索引 Out: [1623329389256, 1623329389256]
接下來,zip(* iters)在 iters 中的每個(gè)迭代器的對(duì)應(yīng)元素對(duì)上返回一個(gè)迭代器。當(dāng)?shù)谝粋€(gè)元素1取自“第一個(gè)”迭代器時(shí),“第二個(gè)”迭代器現(xiàn)在從2開始,因?yàn)樗皇菍?duì)“第一個(gè)”迭代器的引用,因此向前走了一步。因此,zip()生成的第一個(gè)元組是(1,2)。
此時(shí),iters中的所謂 “兩個(gè)”迭代器從3開始,所以當(dāng)zip()從“first”迭代器中拉出3時(shí),它從“second”獲得4以產(chǎn)生元組(3,4)。這個(gè)過程一直持續(xù)到zip()最終生成(9,10)并且iters中的“兩個(gè)”迭代器都用完了:
注意: 這里的"第一個(gè)","第二個(gè)" ,"兩個(gè)"都是指向一個(gè)迭代器,因?yàn)閕d沒有任何變化?。?/strong>
最后我們發(fā)現(xiàn)結(jié)果是一樣的:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] list(better_grouper(nums, 2)) Out: [(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
但是,這里我做了測(cè)試,發(fā)現(xiàn)二者的消耗內(nèi)存是天壤之別,而且在使用iter+zip()的組合后,執(zhí)行速度快了500倍以上,大家感興趣可以自己測(cè)試,把 nums 改成 xrange(100000000) 即可
現(xiàn)在讓我們回顧一下剛剛寫好的better_grouper(lst, n) 方法,不難發(fā)現(xiàn),這個(gè)方法存在一個(gè)明顯的缺陷:如果我們傳遞的n不能被lst的長度整除,執(zhí)行時(shí)就會(huì)出現(xiàn)明顯的問題:
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> list(better_grouper(nums, 4)) [(1, 2, 3, 4), (5, 6, 7, 8)]
我們發(fā)現(xiàn)分組輸出中缺少元素9和10。發(fā)生這種情況是因?yàn)橐坏﹤鬟f給它的最短的迭代次數(shù)耗盡,zip()就會(huì)停止聚合元素。而我們想要的是不丟失任何元素。因此解決辦法是我們可以使用 itertools.zip_longest() 它可以接受任意數(shù)量的 iterables 和 fillvalue 這個(gè)關(guān)鍵字參數(shù),默認(rèn)為None。我們先看一個(gè)簡(jiǎn)單實(shí)例
>>> import itertools as it >>> x = [1, 2, 3, 4, 5] >>> y = ["a", "b", "c"] >>> list(zip(x, y)) # zip總是執(zhí)行完最短迭代次數(shù)停止 [(1, "a"), (2, "b"), (3, "c")] >>> list(it.zip_longest(x, y)) [(1, "a"), (2, "b"), (3, "c"), (4, None), (5, None)]
這個(gè)例子已經(jīng)非常清晰的體現(xiàn)了zip()和 zip_longest()的區(qū)別,現(xiàn)在我們可以優(yōu)化 better_grouper 方法了:
import itertools as it def grouper(lst, n, fillvalue=None): iters = [iter(lst)] * n return it.zip_longest(*iters, fillvalue=fillvalue) # 默認(rèn)就是None
我們?cè)賮砜磧?yōu)化后的測(cè)試:
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> print(list(grouper(nums, 4))) [(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, None, None)]
已經(jīng)非常理想了,各位老鐵們可能還沒有意識(shí)到,我們剛剛所做的一切就是創(chuàng)建itertools 里面grouper方法的全過程!
現(xiàn)在讓我們看看真正的 官方文檔 里面所寫的grouper方法:
和我們寫的基本一樣,除了可以接受多個(gè)iterable 參數(shù),用了*args
最后心滿意足的直接調(diào)用一下:
輸出結(jié)果如下:
暴力求解(brute force)首先基礎(chǔ)概念掃盲,所謂暴力求解是算法中的一種,簡(jiǎn)單來說就是 利用枚舉所有的情況,或者其它大量運(yùn)算又不用技巧的方式,來求解問題的方法。
我在看過暴力算法的廣義概念后,首先想到的居然是盜墓筆記中的王胖子
如果有看過盜墓筆記朋友,你會(huì)發(fā)現(xiàn)王胖子其實(shí)是一個(gè)推崇暴力求解的人,在無數(shù)次遇到困境時(shí)祭出的”枚舉法“,就是暴力求解,例如我印象最深的是云頂天宮中,一行人被困在全是珠寶的密室中無法逃脫,王胖子通過枚舉排除所有可能性,直接得到”身邊有鬼“ 的最終解。PS: 此處致敬南派三叔,和那些他填不上的坑
扯遠(yuǎn)了,回到現(xiàn)實(shí)中來,我們經(jīng)常會(huì)碰到如下的經(jīng)典題目:
你有三張20美元的鈔票,五張10美元的鈔票,兩張5美元的鈔票和五張1美元的鈔票。可以通過多少種方式得到100美元?
為了暴力破解這個(gè)問題,我們只要把所有組合的可能性羅列出來,然后找出100美元的組合即可,首先,讓我們創(chuàng)建一個(gè)list,包含我們手上所有的美元:
bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1]
這里itertools會(huì)幫到我們。 itertools.combinations() 接受兩個(gè)參數(shù)
一個(gè)可迭代的input
正整數(shù)n
最終會(huì)在 input中 n 個(gè)元素的所有組合的元組上產(chǎn)生一個(gè)迭代器。
import itertools as it bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1] result =list(it.combinations(bills, 3)) print(len(result)) # 455種組合 print(result) Out: 455 [(20, 20, 20), (20, 20, 10), (20, 20, 10), ... ]
我僅剩的高中數(shù)學(xué)知識(shí)告訴我其實(shí)這個(gè)就是一個(gè)概率里面的 C 15(下標(biāo)),3(上標(biāo))問題,好了,現(xiàn)在我們擁有了各種組合,那么我們只需要在各種組合里選取總數(shù)等于100的,問題就解決了:
makes_100 = [] for n in range(1, len(bills) + 1): for combination in it.combinations(bills, n): if sum(combination) == 100: makes_100.append(combination)
這樣得到的結(jié)果是包含重復(fù)組合的,我們可以在最后直接用一個(gè)set過濾掉重復(fù)值,最終得到答案:
import itertools as it bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1] makes_100 = [] for n in range(1, len(bills) + 1): for combination in it.combinations(bills, n): if sum(combination) == 100: makes_100.append(combination) print(set(makes_100)) Out:{(20, 20, 10, 10, 10, 10, 10, 5, 1, 1, 1, 1, 1), (20, 20, 10, 10, 10, 10, 10, 5, 5), (20, 20, 20, 10, 10, 10, 5, 1, 1, 1, 1, 1), (20, 20, 20, 10, 10, 10, 5, 5), (20, 20, 20, 10, 10, 10, 10)}
所以最后我們發(fā)現(xiàn)一共有5種方式。 現(xiàn)在讓我們把題目換一種問法,就完全不一樣了:
現(xiàn)在要把100美元的鈔票換成零錢,你可以使用任意數(shù)量的50美元,20美元,10美元,5美元和1美元鈔票,有多少種方法?
在這種情況下,我們沒有預(yù)先設(shè)定的鈔票數(shù)量,因此我們需要一種方法來使用任意數(shù)量的鈔票生成所有可能的組合。為此,我們需要用到itertools.combinations_with_replacement()函數(shù)。
它就像combination()一樣,接受可迭代的輸入input 和正整數(shù)n,并從輸入返回有n個(gè)元組的迭代器。不同之處在于combination_with_replacement()允許元素在它返回的元組中重復(fù),看一個(gè)小栗子:
>>> list(it.combinations_with_replacement([1, 2], 2)) #自己和自己的組合也可以 [(1, 1), (1, 2), (2, 2)]
對(duì)比 itertools.combinations():
>>> list(it.combinations([1, 2], 2)) #不允許自己和自己的組合 [(1, 2)]
所以針對(duì)新問題,解法如下:
bills = [50, 20, 10, 5, 1] make_100 = [] for n in range(1, 101): for combination in it.combinations_with_replacement(bills, n): if sum(combination) == 100: makes_100.append(combination)
最后的結(jié)果我們不需要去重,因?yàn)檫@個(gè)方法不會(huì)產(chǎn)生重復(fù)組合:
>>> len(makes_100) 343
如果你親自運(yùn)行一下,可能會(huì)注意到輸出需要一段時(shí)間。那是因?yàn)樗仨毺幚?6,560,645種組合!這里我們就在執(zhí)行暴力求解
另一個(gè)“暴力” 的itertools函數(shù)是permutations(),它接受單個(gè)iterable并產(chǎn)生其元素的所有可能的排列(重新排列):
>>> list(it.permutations(["a", "b", "c"])) [("a", "b", "c"), ("a", "c", "b"), ("b", "a", "c"), ("b", "c", "a"), ("c", "a", "b"), ("c", "b", "a")]
任何三個(gè)元素的可迭代對(duì)象(比如list)將有六個(gè)排列,并且較長迭代的對(duì)象排列數(shù)量增長得非???。實(shí)際上,長度為n的可迭代對(duì)象有n!排列:
只有少數(shù)輸入產(chǎn)生大量結(jié)果的現(xiàn)象稱為組合爆炸,在使用combination(),combinations_with_replacement()和permutations()時(shí)我們需要牢記這一點(diǎn)。
說實(shí)話,通常最好避免暴力算法,但有時(shí)我們可能必須使用(比如算法的正確性至關(guān)重要,或者必須考慮每個(gè)可能的結(jié)果)
小結(jié)由于篇幅有限,我先分享到這里,這篇文章我們主要深入理解了以下函數(shù)的基本原理:
map()
zip()
itertools.combinations
itertools.combinations_with_replacement
itertools.permutations
在下一篇文章我會(huì)先對(duì)最后三個(gè)進(jìn)行總結(jié),然后繼續(xù)和大家分享itertools里面各種神奇的東西
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/43224.html
前情回顧 大家好,我又回來了。今天我會(huì)繼續(xù)和大家分享itertools這個(gè)神奇的自帶庫,首先,讓我們回顧一下上一期結(jié)尾的時(shí)候我們講到的3個(gè)方法: combinations() combinations_with_replacement() permutations() 讓我們對(duì)這3個(gè)在排列組合中經(jīng)常會(huì)使用到的函數(shù)做個(gè)總結(jié) combinations() 基礎(chǔ)概念 模板:combinations...
摘要:將每一行作為返回,其中是每行中的列名。對(duì)于每一行,都會(huì)生成一個(gè)對(duì)象,其中包含和列中的值。它返回一個(gè)迭代器,是迭代結(jié)果都為的情況。深度解析至此全劇終。 簡(jiǎn)單實(shí)戰(zhàn) 大家好,我又來了,在經(jīng)過之前兩篇文章的介紹后相信大家對(duì)itertools的一些常見的好用的方法有了一個(gè)大致的了解,我自己在學(xué)完之后仿照別人的例子進(jìn)行了真實(shí)場(chǎng)景下的模擬練習(xí),今天和大家一起分享,有很多部分還可以優(yōu)化,希望有更好主意...
摘要:與上面的操作類似,可以使用多種運(yùn)算符和方法來更改集合的內(nèi)容。通過修改集合元素方法運(yùn)算符用法通過修改集合和作用是向集合中添加中所有不存在的元素。 Set是什么 大家好,恰逢初五迎財(cái)神,先預(yù)祝大家新年財(cái)源滾滾?。≡谏弦黄谠斀鈚uple元組的用法后,今天我們來看Python里面最后一種常見的數(shù)據(jù)類型:集合(Set) 與dict類似,set也是一組key的集合,但不存儲(chǔ)value。由于key不...
摘要:上個(gè)月,學(xué)習(xí)群里的同學(xué)問了個(gè)題目,大意可理解為列表降維,例子如下想得到結(jié)果原始數(shù)據(jù)是一個(gè)二維列表,目的是獲取該列表中所有元素的具體值。不經(jīng)意間,函數(shù)的注意事項(xiàng),竟把其它的進(jìn)階內(nèi)容都聯(lián)系起來了。小小的函數(shù),竟成為學(xué)習(xí)之路上的一個(gè)樞紐。 上個(gè)月,學(xué)習(xí)群里的 S 同學(xué)問了個(gè)題目,大意可理解為列表降維 ,例子如下: oldlist = [[1, 2, 3], [4, 5]] # 想得到結(jié)果:...
閱讀 2390·2021-11-24 10:31
閱讀 3443·2021-11-23 09:51
閱讀 2254·2021-11-15 18:11
閱讀 2405·2021-09-02 15:15
閱讀 2465·2019-08-29 17:02
閱讀 2299·2019-08-29 15:04
閱讀 846·2019-08-29 12:27
閱讀 2870·2019-08-28 18:15