你們好,今天想和你們分享一下個人itertools學習體驗及心得,itertools是一個Python的自帶庫,內含多種很是實用的方法,我簡單學習了一下,發現能夠大大提高工做效率,在sf社區內沒有發現十分詳細的介紹,所以但願想本身作一個學習總結。也和朋友們一塊兒分享一下心得html
首先,有關itertools的詳細介紹,我參考的是Python 3.7官方文檔:itertools — Functions creating iterators for efficient looping,你們感興趣能夠去看看,目前尚未中文版本,十分遺憾,這裏不得不吐槽一句,爲啥有日語,韓語,中文的版本沒有跟上呢?python
書規正傳,itertools 我我的評價是Python3裏最酷的東西! 若是你尚未據說過它,那麼你就錯過了Python 3標準庫的一個最大隱藏寶藏,是的,我很快就拋棄了剛剛分享的collections模塊:Python 進階之路 (七) 隱藏的神奇寶藏:探祕Collections,畢竟男人都是大豬蹄子算法
網上有不少優秀的資源可用於學習itertools模塊中的功能。但對我而言,官方文檔自己老是一個很好的起點。學會作甜點以前,老是要會最基礎的麪包。這篇文章即是基本基於文檔概括整理而來。segmentfault
我在學習後的總體感覺是,關於itertools只知道它包含的函數是遠遠不夠的。真正的強大之處在於組合這些功能以建立快速,佔用內存效率極少,漂亮優雅的代碼。app
在這篇很長的文章裏,我會全面覆盤個人學習歷程,爭取全面複製每個細節,在開始以前,若是朋友們還不太知道迭代器和生成器是什麼,能夠參考如下科普掃盲:函數
好啦,坐好扶穩,咱們準備上車了,根據官方文檔的定義:oop
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.
翻譯過來大概就是它是一個實現了許多迭代器構建的模塊,它們受到來自APL,Haskell和SML的構造的啓發......能夠提升效率啥的,學習
這主要意味着itertools中的函數是在迭代器上「操做」以產生更復雜的迭代器。
例如,考慮內置的zip()函數,該函數將任意數量的iterables做爲參數,並在其相應元素的元組上返回迭代器:測試
print(list(zip([1, 2, 3], ['a', 'b', 'c']))) Out:[(1, 'a'), (2, 'b'), (3, 'c')]
這裏讓咱們思考一個問題,這個zip函數究竟是如何工做的?優化
與全部其餘list同樣,[1,2,3] 和 ['a','b','c'] 是可迭代的,這意味着它們能夠一次返回一個元素。
從技術上講,任何實現:
方法的Python對象都是可迭代的。若是對這方面有疑問,你們能夠看前言部分提到的教程哈
其實有關iter()這個內置函數,當在一個list或其餘可迭代的對象 x 上調用時,會返回x本身的迭代器對象:
iter([1, 2, 3, 4]) iter((1,2,3,4)) iter({'a':1,'b':2}) Out:<list_iterator object at 0x00000229E1D6B940> <tuple_iterator object at 0x00000229E3879A90> <dict_keyiterator object at 0x00000229E1D6E818>
實際上,zip()函數經過在每一個參數上調用iter(),而後使用next()推動iter()返回的每一個迭代器並將結果聚合爲元組來實現。 zip()返回的迭代器遍歷這些元組
而寫到這裏不得不回憶一下,以前在 Python 進階之路 (五) map, filter, reduce, zip 一網打盡我給你們介紹的神器map()內置函數,其實某種意義上也是一個迭代器的操做符而已,它以最簡單的形式將單參數函數一次應用於可迭代的sequence的每一個元素:
list(map(len, ['xiaobai', 'at', 'paris'])) Out: [7, 2, 5]
參考map模板,不難發現:map()函數經過在sequence上調用iter(),使用next()推動此迭代器直到迭代器耗盡,並將func 應用於每步中next()返回的值。在上面的例子裏,在['xiaobai', 'at', 'paris']的每一個元素上調用len(),從而返回一個迭代器包含list中每一個元素的長度
因爲迭代器是可迭代的,所以能夠用 zip()和 map()在多個可迭代中的元素組合上生成迭代器。
例如,如下對兩個list的相應元素求和:
a = [1, 2, 3] b = [4, 5, 6] list(map(sum, zip(a,b))) Out: [5, 7, 9]
這個例子很好的解釋瞭如何構建itertools中所謂的 「迭代器代數」 的函數的含義。咱們能夠把itertools視爲一組構建磚塊,能夠組合起來造成專門的「數據管道」,就像這個求和的例子同樣。
其實在Python 3裏,若是咱們用過了map() 和 zip() ,就已經用過了itertools,由於這兩個函數返回的就是迭代器!
咱們使用這種 itertools 裏面所謂的 「迭代器代數」 帶來的好處有兩個:
可能有朋友對這兩個好處有所疑問,不要着急,咱們能夠分析一個具體的場景:
如今咱們有一個list和正整數n,編寫一個將list 拆分爲長度爲n的組的函數。爲簡單起見,假設輸入list的長度可被n整除。例如,若是輸入= [1,2,3,4,5,6] 和 n = 2,則函數應返回 [(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)]
咱們進行簡單的測試,結果正確:
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)]
可是問題來了,若是咱們試圖傳遞一個包含1億個元素的list時會發生什麼?咱們須要大量內存!即便有足夠的內存,程序也會掛起一段時間,直到最後生成結果
這個時候若是咱們使用itertools裏面的迭代器就能夠大大改善這種狀況:
def better_grouper(lst, n): iters = [iter(lst)] * n return zip(*iters)
這個方法中蘊含的信息量有點大,咱們如今拆開一個個看,表達式 [iters(lst)] * n 建立了對同一迭代器的n個引用的list:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] iters = [iter(nums)] * 2 list(id(itr) for itr in iters) # Id 沒有變化,就是建立了n個索引 Out: [1623329389256, 1623329389256]
接下來,zip(* iters)在 iters 中的每一個迭代器的對應元素對上返回一個迭代器。當第一個元素1取自「第一個」迭代器時,「第二個」迭代器如今從2開始,由於它只是對「第一個」迭代器的引用,所以向前走了一步。所以,zip()生成的第一個元組是(1,2)。
此時,iters中的所謂 「兩個」迭代器從3開始,因此當zip()從「first」迭代器中拉出3時,它從「second」得到4以產生元組(3,4)。這個過程一直持續到zip()最終生成(9,10)而且iters中的「兩個」迭代器都用完了:
注意: 這裏的"第一個","第二個" ,"兩個"都是指向一個迭代器,由於id沒有任何變化!!
最後咱們發現結果是同樣的:
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)]
可是,這裏我作了測試,發現兩者的消耗內存是天壤之別,並且在使用iter+zip()的組合後,執行速度快了500倍以上,你們感興趣能夠本身測試,把 nums 改爲 xrange(100000000) 便可
如今讓咱們回顧一下剛剛寫好的better_grouper(lst, n) 方法,不難發現,這個方法存在一個明顯的缺陷:若是咱們傳遞的n不能被lst的長度整除,執行時就會出現明顯的問題:
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> list(better_grouper(nums, 4)) [(1, 2, 3, 4), (5, 6, 7, 8)]
咱們發現分組輸出中缺乏元素9和10。發生這種狀況是由於一旦傳遞給它的最短的迭代次數耗盡,zip()就會中止聚合元素。而咱們想要的是不丟失任何元素。所以解決辦法是咱們可使用 itertools.zip_longest() 它能夠接受任意數量的 iterables 和 fillvalue 這個關鍵字參數,默認爲None。咱們先看一個簡單實例
>>> import itertools as it >>> x = [1, 2, 3, 4, 5] >>> y = ['a', 'b', 'c'] >>> list(zip(x, y)) # zip老是執行完最短迭代次數中止 [(1, 'a'), (2, 'b'), (3, 'c')] >>> list(it.zip_longest(x, y)) [(1, 'a'), (2, 'b'), (3, 'c'), (4, None), (5, None)]
這個例子已經很是清晰的體現了zip()和 zip_longest()的區別,如今咱們能夠優化 better_grouper 方法了:
import itertools as it def grouper(lst, n, fillvalue=None): iters = [iter(lst)] * n return it.zip_longest(*iters, fillvalue=fillvalue) # 默認就是None
咱們再來看優化後的測試:
>>> 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)]
已經很是理想了,各位老鐵們可能尚未意識到,咱們剛剛所作的一切就是建立itertools 裏面grouper方法的全過程!
如今讓咱們看看真正的 官方文檔 裏面所寫的grouper方法:
和咱們寫的基本同樣,除了能夠接受多個iterable 參數,用了*args
最後心滿意足的直接調用一下:
輸出結果以下:
首先基礎概念掃盲,所謂暴力求解是算法中的一種,簡單來講就是 利用枚舉全部的狀況,或者其它大量運算又不用技巧的方式,來求解問題的方法。
我在看過暴力算法的廣義概念後,首先想到的竟然是盜墓筆記中的王胖子
若是有看過盜墓筆記朋友,你會發現王胖子實際上是一個推崇暴力求解的人,在無數次遇到困境時祭出的」枚舉法「,就是暴力求解,例如我印象最深的是雲頂天宮中,一行人被困在全是珠寶的密室中沒法逃脫,王胖子經過枚舉排除全部可能性,直接獲得」身邊有鬼「 的最終解。PS: 此處致敬南派三叔,和那些他填不上的坑
扯遠了,回到現實中來,咱們常常會碰到以下的經典題目:
你有三張20美圓的鈔票,五張10美圓的鈔票,兩張5美圓的鈔票和五張1美圓的鈔票。能夠經過多少種方式獲得100美圓?
爲了暴力破解這個問題,咱們只要把全部組合的可能性羅列出來,而後找出100美圓的組合便可,首先,讓咱們建立一個list,包含咱們手上全部的美圓:
bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1]
這裏itertools會幫到咱們。 itertools.combinations() 接受兩個參數
最終會在 input中 n 個元素的全部組合的元組上產生一個迭代器。
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), ... ]
我僅剩的高中數學知識告訴我其實這個就是一個機率裏面的 C 15(下標),3(上標)問題,好了,如今咱們擁有了各類組合,那麼咱們只須要在各類組合裏選取總數等於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)
這樣獲得的結果是包含重複組合的,咱們能夠在最後直接用一個set過濾掉重複值,最終獲得答案:
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)}
因此最後咱們發現一共有5種方式。 如今讓咱們把題目換一種問法,就徹底不同了:
如今要把100美圓的鈔票換成零錢,你可使用任意數量的50美圓,20美圓,10美圓,5美圓和1美圓鈔票,有多少種方法?
在這種狀況下,咱們沒有預先設定的鈔票數量,所以咱們須要一種方法來使用任意數量的鈔票生成全部可能的組合。爲此,咱們須要用到itertools.combinations_with_replacement()函數。
它就像combination()同樣,接受可迭代的輸入input 和正整數n,並從輸入返回有n個元組的迭代器。不一樣之處在於combination_with_replacement()容許元素在它返回的元組中重複,看一個小栗子:
>>> list(it.combinations_with_replacement([1, 2], 2)) #本身和本身的組合也能夠 [(1, 1), (1, 2), (2, 2)]
對比 itertools.combinations():
>>> list(it.combinations([1, 2], 2)) #不容許本身和本身的組合 [(1, 2)]
因此針對新問題,解法以下:
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)
最後的結果咱們不須要去重,由於這個方法不會產生重複組合:
>>> len(makes_100) 343
若是你親自運行一下,可能會注意到輸出須要一段時間。那是由於它必須處理96,560,645種組合!這裏咱們就在執行暴力求解
另外一個「暴力」 的itertools函數是permutations(),它接受單個iterable併產生其元素的全部可能的排列(從新排列):
>>> list(it.permutations(['a', 'b', 'c'])) [('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'), ('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')]
任何三個元素的可迭代對象(好比list)將有六個排列,而且較長迭代的對象排列數量增加得很是快。實際上,長度爲n的可迭代對象有n!排列:
只有少數輸入產生大量結果的現象稱爲組合爆炸,在使用combination(),combinations_with_replacement()和permutations()時咱們須要牢記這一點。
說實話,一般最好避免暴力算法,但有時咱們可能必須使用(好比算法的正確性相當重要,或者必須考慮每一個可能的結果)
因爲篇幅有限,我先分享到這裏,這篇文章咱們主要深刻理解了如下函數的基本原理:
在下一篇文章我會先對最後三個進行總結,而後繼續和你們分享itertools裏面各類神奇的東西