今天分享一下我本身的itertools學習體驗,itertools是一個Python的自帶庫,內含多種很是實用的方法,經過基礎瞭解後,我發現能夠大大提高工做效率。html
首先,有關itertools的詳細介紹,我參考的是Python 3.7官方文檔:itertools — Functions creating iterators for efficient looping,你們感興趣能夠去看看,目前尚未中文版本,十分遺憾,這裏不得不吐槽一句,爲啥有日語,韓語,中文的版本沒有跟上呢?python
書規正傳,itertools 我認爲是Python3裏最酷的東西!算法
若是你尚未據說過它,那麼你就錯過了Python3標準庫的一個最大隱藏寶藏,是的,我很快就拋棄了剛剛分享的collections模塊:小白的Python 學習筆記(七)神奇寶藏 Collections,畢竟男人都是大豬蹄子bash
網上有不少優秀的資源可用於學習itertools模塊中的功能。但我認爲官方文檔老是一個很好的起點。這篇文章即是基本基於文檔概括整理而來。app
我在學習後的總體感覺是,關於itertools只知道它包含的函數功能足矣,不必定非要較真。真正的強大之處在於組合這些功能以建立快速,佔用內存效率極少,漂亮優雅的代碼。函數
在這篇很長的文章裏,我會全面回顧個人學習歷程,爭取全面複製每個細節,在開始以前,若是朋友們還不太知道迭代器和生成器是什麼,能夠參考如下科普掃盲:oop
坐好扶穩,咱們準備上車了,根據官方文檔的定義:post
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'] 是可迭代的,這意味着它們能夠一次返回一個元素。 從技術上講,任何實現:
.__ iter __()
或 .__ getitem __()
方法的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()從「第一個」迭代器中拉出3時,它從「第二個」得到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 改爲 range(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
最後心滿意足的測試一下:
from itertools import zip_longest
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
test_list = list(grouper("ABCDEFG",3))
test_tuple = tuple(grouper(range(0,7),2,'Null'))
test_dict = dict(zip(test_list,test_tuple))
複製代碼
輸出結果:
print(test_list)
Out:[('A', 'B', 'C'), ('D', 'E', 'F'), ('G', None, None)]
複製代碼
print(test_tuple)
Out:((0, 1), (2, 3), (4, 5), (6, 'Null'))
複製代碼
print(test_dict)
Out: {('A', 'B', 'C'): (0, 1), ('D', 'E', 'F'): (2, 3), ('G', None, None): (4, 5)}
複製代碼
首先基礎概念掃盲,所謂暴力求解是算法中的一種,簡單來講就是 利用枚舉全部的狀況,或者其它大量運算又不用技巧的方式,來求解問題的方法。 我在看過暴力算法的廣義概念後,首先想到的竟然是盜墓筆記中的王胖子
若是有看過盜墓筆記朋友,你會發現王胖子實際上是一個推崇暴力求解的人,在無數次遇到困境時祭出的」枚舉法「,就是暴力求解,例如我印象最深的是雲頂天宮中,一行人被困在全是珠寶的密室中沒法逃脫,王胖子經過枚舉排除全部可能性,直接獲得」身邊有鬼「 的最終解。
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裏面各類神奇的東西