小白的Python 學習筆記(九)itertools深度解析,滿滿的乾貨(上)

前言

今天分享一下我本身的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

神奇的itertools##

坐好扶穩,咱們準備上車了,根據官方文檔的定義: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的每一個元素:

  • 模板: map(func,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 裏面所謂的 「迭代器代數」 帶來的好處有兩個:

  1. 提升內存效率 (lazy evaluation)
  2. 提速

可能有朋友對這兩個好處有所疑問,不要着急,咱們能夠分析一個具體的場景:

如今咱們有一個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方法:

enter image description here

和咱們寫的基本同樣,除了能夠接受多個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)}
複製代碼

暴力求解(brute force)

首先基礎概念掃盲,所謂暴力求解是算法中的一種,簡單來講就是 利用枚舉全部的狀況,或者其它大量運算又不用技巧的方式,來求解問題的方法。 我在看過暴力算法的廣義概念後,首先想到的竟然是盜墓筆記中的王胖子

若是有看過盜墓筆記朋友,你會發現王胖子實際上是一個推崇暴力求解的人,在無數次遇到困境時祭出的」枚舉法「,就是暴力求解,例如我印象最深的是雲頂天宮中,一行人被困在全是珠寶的密室中沒法逃脫,王胖子經過枚舉排除全部可能性,直接獲得」身邊有鬼「 的最終解。

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

最終會在 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!排列:

enter image description here

只有少數輸入產生大量結果的現象稱爲組合爆炸,在使用combination(),combinations_with_replacement()和permutations()時咱們須要牢記這一點。

說實話,一般最好避免暴力算法,但有時咱們可能必須使用(好比算法的正確性相當重要,或者必須考慮每一個可能的結果)

小結

因爲篇幅有限,我先分享到這裏,這篇文章咱們主要深刻理解了如下函數的基本原理:

  • map()
  • zip()
  • itertools.combinations
  • itertools.combinations_with_replacement
  • itertools.permutations

在下一篇文章我會先對最後三個進行總結,而後繼續和你們分享itertools裏面各類神奇的東西

相關文章
相關標籤/搜索