【讀書筆記】《Python_Cookbook3》第一章:數據結構和算法

 
Python提供了多樣化有用的內建數據結構,例如列表、集合、字典。大多數時候,這些結構的使用比較簡單,而後,一些關於搜索、排序、過濾的常見問題常常出現。本章節的目標是討論常見的數據結構,以及涉及到的數據算法。另外,介紹模塊集合中多樣的數據結構
 
1.1將序列解析成不一樣變量
問題:有N個元素的集合或列表,想要將它解析成N個變量
解決方法:
任何序列(或者迭代)能夠經過簡單的運算解析成不一樣的變量。要求變量的數量和序列的結構相匹配(個數等一致),以下面的例子
【元祖】
【列表】
若是變量數量和元素個數不符,將會報錯,好比:
討論:不是隻有元祖和列表能夠解析,任何對象發生迭代時均可以用解析。包括字符串、文件、迭代器、生成器。例以下面的例子:
解析時,有可能你不想要解析全部的內容,Python爲此沒有提供特殊的語法,可是你能夠用一個無用的變量名來替換它。例如:
可是注意:這個不要的變量名字必定不要和已經使用的變量名相同,不然數據會被覆蓋
 
1.2任意長度的迭代器元素解析
問題:你須要解析迭代器的N個元素,可是迭代器的實際元素可能比N長,會引發「too many values to unpack」的異常
解決辦法:
Python的*表達式能夠解決這個問題。舉個例子,年級結束時,你決定計算你課程分數,去掉第一次和最後一次的分數,取平均值。若是隻有4次做業,你能夠簡單的解析成4個變量,可是若是有24個做業呢?一個*表達式能夠簡單的解決這個問題:
另外一個例子,假如你有一個用戶記錄,包括姓名、郵箱、多個電話號碼,你能夠像下面這樣去解析這個記錄:
注意,不論電話號碼多長(即便是0個),解析出來的始終是一個列表list。*變量也能夠解析列表開始位置的變量。例如一個公司有一份8個季度的價格報表,想統計前7個季度的價格平均值,你能夠像下面這麼作:
討論:擴展迭代解析是爲了更好的解析不定長度的迭代器。迭代器的結構常常是已知的(好比最後一個元素是電話號碼),*解析使開發更容易解析出迭代器中的元素。
*語法可以更好用的解析可變序列。好比,下面的由不一樣元祖組成的序列:
*解析也能夠結合字符串處理操做進行解析,例如字符串的split分割:
有時你想拋棄一些元素,你可使用一個*變量去代替一些要拋棄的變量名,你可使用一個通用的拋棄變量名(好比*_,或者*ign),例如:
*解析能夠幫助列表進行更多的功能處理。例如,你能夠簡單分開列表的頭部和尾部部分:
利用這個能夠寫出更好的遞歸算法,好比:
(解釋一下這句話head+sum(tail) if tail else head:意思是若是存在tail就返回head+sum(tail)不然返回head,這樣就知足了當遞歸到最後一個數時,實際*tail是一個空,則只返回了head,達到了遞歸求和的效果)
由於遞歸的限制,遞歸沒有展現Python強大的特性,最後一個例子只是實踐了一下學術的好奇心。
 
1.3保留最後N個元素
問題:想要保留迭代器或其餘處理的最後幾項元素歷史記錄
解決方法:能夠用集合隊列來保存歷史。例如,下面的代碼完成了一個簡單的文本匹配隊列中的行,當在隊列的前N行中匹配到文本時經過yield記錄
討論:搜索的代碼。一般使用涉及yield的迭代方法,就像上面的例子,將使用搜索的結果和搜索過程分離開。若是想知道更多關於迭代器的能夠查看本書的4.3(4章節)
用deque(maxlen=N)建立一個固定大小的隊列,當隊列滿了時進來新數據,會將最先進來的數據移除出去(先進先出)。例如:
雖然也可使用list進行這樣的操做(好比增長或刪除),可是使用隊列deque會更優雅,且速度更快
在須要任何一種簡單的隊列結構時均可以使用deque,若是不給deque一個最大的限制長度,你能夠得到一個無限大小的隊列,能夠對隊列進行append和pop操做,例如:
在deque中增長或刪除元素的複雜度是O(1),可是在list中插入或移除元素的複雜度是O(N)
 
1.4查找最大或最小的N個元素
問題:想要將集合中最大的或最小的N個元素放到一個list中
解決方法:heapq模塊有兩個方法,nlargest()和nsmallest()。這兩個方法能夠實現咱們想要的功能。例如:
這兩個方法容許經過一個叫key的參數來使他們能夠去處理更加複雜的數據結構,例如:
討論:若是要查找N個最大或最小值時,N超過了集合的數量,這個模塊提供了一個更優秀的方法。先將數據轉換到一個list中,而後會像一個heap堆同樣對它進行排序,例如:
這個heap最重要的特色是heap[0]始終是最小的一個元素,此外你能夠經過heapq.heappop()更容易的找到元素,經過它能夠將heap底部的元素刪除(最小的值),這個操做的複雜度是O(log N),N爲heap的長度。例如,找到最小的三個元素,你能夠這樣寫:
nlargest()和nsmallest()方法更適合去尋找數據中相對最小的值,若是你只想找一個最小的或最大的值(N=1),這兩個方法比min()和max()更快。可是,若是N和集合自己的長度相同(對整個集合進行排序),這兩個方法沒有sorted(items)或者使用切片sorted(:N)快。
必須根據實際狀況來肯定使用最優的方法(好比N和輸入的數據長度接近時,使用sort排序)
雖然它不是必須使用的菜譜,heap的實現仍是值得去學習的,heap常常出如今正規的算法和數據結構書上,這篇關於heapq模塊的文章只是討論了一下底層的實現信息。
 
1.5實現一個優先級隊列
問題:想要實現一個根據給定的優先級去排序的隊列,而後每次pop都返回最高優先級的數據
解決方法:下面的類經過heapq實現了一個簡單的優先級隊列
說明:若是優先級priority用正數是pop()出來的是最小的數,用負數pop()出來的是最大的數,因此這裏寫做-priority
接下來是一個使用該類的例子:
能夠看到pop()出來的是優先級最高的,一樣能夠看到相同優先級的,pop()先出來先插入到隊列的數
討論:這段代碼更關心的是heapq模塊,heapq.heappush()和heapq.heappop()這兩個方法,插入和移除隊列數據,第一個元素是最低優先級的(能夠參考本書第1.4節)。heappop()老是返回優先級最低的數,push和pop操做的空間複雜度是O(log N),N是heap的長度,因此即便heap數據不少效果也會很好。
在本書中,隊列由元祖(-priority,index,item)組成,優先級取的最高優先級,這和默認的排序是相反的。
index變量是爲了標識相同優先級的項,index是自增的,相同優先級的項排序與他們插入的順序有關,index在比較相同優先級項的操做過程當中扮演了一個重要的角色。
舉一個不能排序的例子:
若是使用(priority,item)格式的元祖,他能夠比較不一樣優先級的項,可是不能比較相同優先級的項,例如:
經過增長index組成的(priority,index,item)元祖,能夠解決相同優先級的問題(Python歷來不用困惱比較的問題)。
若是想將隊列用在線程通訊,須要增長優先級去鎖定或發信號(詳見本書12.3章節),這篇文章更深層次的介紹了heapq模塊的原理和heaps的實現
 
1.6字典中實現key映射一個複雜的values值
問題:使用字典,一個key想對應多個value(也叫做multidict)
解決方法:字典是一個key對一個value的映射。若是想要key映射多個value,能夠將多個value存儲到list或者set容器中。例如,下面的例子。
根據用途去選擇存儲到列表list和集合set中。若是你想要記住排序就用list,若是想要去重(且不在意排序)就用集合set
經過collections模塊的defaultdict方法更容易的構建字典結構,defaultdict自動初始化value的類型,你只要添加元素就能夠了。例如:
注意defaultdict直接建立一個字典的key的value類型(及時它如今沒有在字典中建立)。若是你不想要這個特性,你能夠用字典的setdefault()方法來替換。好比下面的例子。
而後,不少程序發現setdefault()有一個很差的地方——每次使用以前都須要初始化一下實例(例子中的list[])
討論:理論上構造一個字典是簡單的,而後,讓字典本身去初始化是比較麻煩的。例如,你可能像下面這樣去寫代碼:
使用defaultdict可使得代碼更加簡潔:
本書極力去解決數據處理中組織記錄的問題,例如本書1.15章節的例子
 
1.7保留字典的順序
問題:建立一個字典,而且想要控制字典的順序,以方便迭代和序列化
解決方法:使用collections模塊中的OrderedDict方法能夠控制字典中元素的排序,該方法準確的記錄了數據插入的順序,例如:
建立一個字典,OrderedDict後續能夠很方便的對數據進行序列化或從新定義數據的格式操做。例如,想要嚴格控制JSON格式的域的出現順序,首先使用OrderedDict存儲數據能夠實現它。
討論:OrderedDict內部原理是使用一個雙向鏈表保持插入順序。插入新元素時會在尾部插入,再插入已存在key的數據時不會改變排序。
注意OrderedDict的長度是普通字典的兩倍多,由於額外的建立了一個鏈表。因此你須要根據你的應用程序的需求判斷是否要增長額外的內存消耗來使用OrderedDict。
 
1.8字典的計算
問題:對字典數據進行多樣的計算(好比求最小值、最大值、排序等)
解決方法:建立一個價格字典,將股票名稱映射到價格
爲了更好的計算字典的數據,比較好用的方法是使用zip()去轉換字典的key和value。下面的例子是將怎麼找到最小和最大價格以及對應的股票名稱
同理,使用zip()和sorted()來進行字典的排序,像下面這樣:
再作這種計算式,要注意zip()建立的 迭代只能使用一次,例以下面的錯誤事例:
討論:若是嘗試在字典裏執行普通的數據減小,你會發現程序只會打印出key,value沒有被打印出來,好比下面的例子
而後大多數時候咱們須要的某一信息對應的key(好比哪一個股票的有最低的價格?)
咱們能夠經過應用一個去的key的函數來得到最小或最大值的key,例如:
zip()解決了字典成對插入數據到列表的問題。當比較元祖時,value元素將先被比較,而後纔是key進行比較。爲咱們作排序給出了極大的方便。
必須注意相同value的排序,他會根據key的排序給出排序,好比下面的例子:
說明一下zip(),zip()會對列表進行壓縮成一個列表,每一項都是一個元祖,他會將每一個列表的list[i]位置的組合成一個元祖,並放在i位置,注意zip()組成的列表長度是壓縮的列表的最短的一個的長度。
 
1.9查找兩個字典中的共同點
問題:有兩個字典,想查找兩個字典中相同的地方(key或者value)
解決方法:有兩個字典:
去查找兩個字典中相同的地方,經過使用keys()和items()兩個方法的set集合操做來實現,例如:
這種操做能夠實現改變或過濾字典內容。好比,想要將選中的key移到一個新的字典中,下面是一個簡單的例子:
討論:字典是一系列的key和value的映射,keys()方法返回了字典的所有的key(keys-view對象)。它支持集合操做,好比並集交集差集。這樣當你想對字典進行集合操做時,能夠直接使用keys-view對象。而不用先將他們轉換成set集合了。
items()返回的是字典所有的(key,value)對(keys-view對象)。這個對象也支持簡單的set集合操做。
雖然values()和這相似,可是values()不支持set集合操做。某種程度上。items()包含的values不具備keys()的惟一性,使得作集合操做時可能會產生一些問題。若是必定要這麼作運算,建議先轉換成set集合再進行操做
 
1.10從序列中移除重複數據且保持序列的順序
問題:想要移除序列中重複的值,又想保持序列元素的順序
解決方法:若是序列中的值是hashable的(hashable:能夠當作字典中的key),這個問題能夠用set和迭代很容易的解決,好比:
下面是一個使用這個方法的例子:
這個只有序列值是hashable時能夠用,若是要去重unhashable類型的(例如字典),你能夠像下面這樣作一點改變:
這裏指定key參數是爲了將序列的元素轉成成哈希結構的重複數據,
解釋一下這個方法:val=item if key is None else key(item) 若是key是None則val=item,若是key不爲None則val=key(item);yield只能在函數中用,能夠用作迭代,保存了數據的存儲順序
下面展現它是怎麼工做的:
看看實例是怎麼用的:key=lambda d:(d['x'],d['y'])實際是取出序列中每一個字典的x和y的值組成一個元祖,既然key確定不爲None,則val=key(item),即取出每一個子字典中的x和y的值組成元祖,而後插入到set屬性的seen集合中,seen始終去重。經過yield item保存了迭代結果(既包括值,又包括順序)
複雜結構的數據去重處理,第二種方法的效果更好。
討論:若是隻是想去重不在乎排序,能夠用set直接實現。例如:
這種方法不會保持排序,所得結果順序雜亂。上面的方法能夠解決這個問題。上面的方法不只可以處理列表,也能夠處理其餘的方法,好比去重讀取的文件行:
方法中的key模仿了內建函數sorted()、min()和max(),例如本書的1.8和1.13章節
 
1.11命名切片
問題:程序代碼是不值得讀的硬編碼切片,想要清除它
解決方法:將記錄中特殊的數據使用固定的格式提取出來(例如從一個f文件或者相似的格式)
替換上面的作法,爲何不這麼命名切片呢?
在第二個版本中,咱們避免了使用硬編碼,使的咱們想作什麼變得更加清晰。
討論:通常來講,大量的硬編碼會影響易讀性,假如一年後你再讀這些代碼,你會想當初寫這段代碼的時候你在想什麼?而咱們的解決方法可使得代碼更加清晰。
內建函數slice()能夠建立一個切片對象,用在任何容許使用切片的地方,好比:
若是有一個切片實例s,你能夠同過s.start、s.stop、s.step屬性得到更多的信息,好比:
start爲切片起始位置,stop爲切片終止位置,step爲切片的步長
另外,經過indices(size)能夠映射切片到一個特殊長度序列上,它將返回一個元祖(start,stop,step),這些值被合適的限定在界限範圍內(能夠避免索引時報IndexError異常),例如:
indices(size)解釋:根據測試發現,他不會改變原有切片對象的step,若是start或者stop超出要適應的特殊序列s的長度,start會變成len(s),以保證切片索引不會超出size報異常,以下:
 
1.12將出現比較頻繁的元素放到一個序列中
問題:有一個序列,想要將其中出現比較頻繁的元素放到一個序列中。
解決方法:collections.Counter這個類就是爲了解決這個問題,它和most_common()方法一塊兒解決這個問題。
舉個例子,要找出一個序列中出現次數最多的詞,咱們能夠這麼解決:
討論:根據輸入的元素,Counter對象能夠處理輸入元素中全部的hashable元素,一個Counter是元素映射它出現次數的字典,例如:
若是想要手動實現數量自增(累加其餘序列中的關鍵詞出現的次數),能夠用下面的方法:
也可使用Counter的update()方法來實現上面的功能:
Counter實例有一個不爲人知的特性,能夠結合數學運算,例如:
Counter對象是一個統計數據很是有利的一個工具,你應該更喜歡經過字典去手動解決問題。
 
1.13經過字典的某一個key對字典的列表進行排序
問題:根據一個或多個字典域的值來對字典列表進行排序
解決方法:這種類型的問題,能夠經過operator模塊的itemgetter方法來解決。假設從數據庫中查詢出數據用到網站上,返回的數據格式以下:
經過字典中任何一個域進行排序都是很是容易的,例如:
(說明:itemgetter反回了對應key的value,若是是數字則是返回對應位置的值;sorted(data,key)是指date數據按照key關鍵詞進行排序)
itemgetter()方法也能夠接受多個關鍵詞,好比下面的代碼:
itemgetter能夠這麼分開寫:
a=itemgetter('lname')
for i in rows:
     print(a(i))
討論:在這個例子中,rows傳遞給內建函數sorted(),sorted()接收了參數key,key參數是爲了傳遞給rows做爲輸入,而後回調rows按照key的值進行排序的結果。itemgetter()函數就是建立了這樣的一個key參數。
operation.itemgetter()函數得到rows中的一個指望值。它能夠是一個字典的域名,一個list元素的數值,或者任何一個能夠經過對象的__getitem__()方法得到的值。若是給itemgetter()傳遞多個參數,會將返回的多個元素放到一個元祖中,而且sorted()將根據這個元祖進行排序。這對於經過多個域同時進行排序是很是有用的(好比名或行,就像上面的例子同樣)
itemgetter()有時用lambda表達式進行代替,好比:
這個解決方案也很好,可是使用itemgetter()會更快。因此你能夠處於性能的考慮來選擇。
最後,但也很重要的,能夠將itemgetter()應用到本書講過的min()和max(),例如:
 
1.14對不支持比較操做的類進行排序
問題:想要對類進行排序,可是類自己不支持排序操做
解決方法:使用內建函數sorted()中的key參數指定對象中的一個可調用的關鍵詞,而後按照key進行排序。例如,程序中有一個User序列實例,而後想按照User的屬性user_id進行排序,這樣咱們須要提供一個User實例做爲輸入,而後返回user_id,例如:
能夠用operator.attrgetter()來替換lambda
討論:根據我的喜愛來選擇使用lambda仍是attrgetter()。可是attrgetter()會稍微快一點,而且支持同時提取多個域,它和本書1.13節中講的operator.itemgetter()相似。例如,若是User實例有first_name和last_name屬性,你能夠像下面這樣進行排序:
一樣的,咱們也能夠在min()和max()中使用這個方法,例如:
 
1.15按照某一個域進行分組
問題:有一個字典或者實例的序列,想要按照某個域的值進行分組去迭代數據,好比日期。
解決方法:itertools.groupby()函數能夠用來對數據進行分組,好比下面的字典列表的數據。
如今支持經過date分組而後進行迭代,首先須要經過域來進行排序(這個例子的域是date),而後使用itertools.groupby():
討論:groupby()函數經過遍歷序列和尋找定義的值(或者經過給定的key方法返回的值)來工做。每次循環都返回這個值以及按照這個值進行的分組(值相同的爲一組)。
一個重要的步驟是先要將數據按照這個域進行排序,由於groupby()只能處理連續的數據,若是不先進行排序將不能正確完成分組。
若是隻是想對數據進行簡單分組而不在乎順序,使用defaultdice()建立一個multidict,就像本書1.6章節總描述的,例如:
每一個date下的記錄能夠這樣用:
像後面這個例子,不須要先對數據進行排序。若是不考慮內存,第二種方法比第一種先排序在用groupby()來進行分組的速度快。
 
1.17提取字典的子集
問題:想要提取一個字典的子集存儲到另外一個字典中
解決方法:經過字典推導能夠很輕鬆的完成,例如:
討論:字典推導能夠實現的也能夠經過建立一個元祖序列,而後傳遞給dict()建立字典來實現,例如:
可是字典推導的方法更快更簡潔(是使用dict()兩倍快)
有多重方法能夠實現一樣的事情,好比第二個例子能夠重寫成下面的樣子:
可是研究代表,這種方式比第一種方式慢1.6倍。性能問題須要花更多的時間去學習。本書14.13介紹了時間和性能。
 
1.18映射名稱到序列元素
問題:咱們經過位置讀取列表或元祖的順序,可是有時候難以讀取,而後想要數據的結構對位置的依賴小一點,經過名稱來得到元素
解決方法:collections.namedtuple()提供了這個方法。經過使用元祖對象的object.collections.namedtuple()工廠方法返回了一個標準Python元祖類型的子類,這種方法花銷更小。提供一個類型名稱和須要的域,而後他會返回一個能夠實例化的類,將值放入定義的域中。例如:
雖然一個namedtuple的實例看起來和class實例很像,可是它支持全部元祖的操做,好比索引和解析,例如:
元祖命名的一個主要用法是將元素的位置和操做進行解隅。這樣若是你從數據庫中得到一個大的元祖列表,而後經過訪問元素的位置來進行操做,若是你在表中增長一個新字段你的代碼就不能用了,如今若是返回一個元祖和元祖的名稱就能夠解決這種方法。
舉個使用元祖排序進行操做的例子:
參照元素位置使得代碼不容易理解而且依賴記錄的結構。接下來是使用namedtuple的版本 :
若是例子中的records列表已經包含這樣的實例,就能夠不用經過namedtuple對Stock進行轉換了
討論:namedtuple能夠當作字典的替換使用,且存儲空間比字典小。若是想要構建涉及到字典的大數據結構,使用namedtuple將更有效。可是藥注意,與字典不一樣的是namedtuple是不可變的。好比:
若是想要改變某一個屬性,須要經過_replace()方法來更改namedtuple實例的屬性,它建立了一個新的namedtuple,值被替換了。例如:
_replace()能夠很方便的去填充namedtuple的可選域,這樣能夠建立一個包含默認值的元祖模型,而後經過_replace()來建立一個新的實例而且替換值。例如:
 
(方法中使用*s表示全部參數存在元祖中;**s表示全部參數存在字典中)
下面演示一下怎麼使用上面的代碼:
若是構建的數據結構有不少個屬性須要變化,不建議使用namedtuple,能夠考慮用__slots__(本書8.4章)
 
1.19同時轉換並計算數據
問題:須要執行彙集函數(好比sum(),min(),max()),可是首先須要篩選和轉換數據
解決方法:一個結合數據轉換和計算的很是優雅的方法,使用生成器表達式傳遞參數。例如,若是想要計算平方和,能夠像下面這樣:
下面是另外一個例子:
討論:這個解決方案展現了將生成器表達式 做爲函數的單個參數的精妙之處(好比你不須要重複操做)。下面的聲明效果相同:
 
使用生成器做爲參數比建立一個臨時的列表更有效、更優雅。好比,若是不用生成器表達式 ,你可能像下面這樣作:
在這個例子中使用了額外的步驟建立了一個額外的列表。若是是小列表影響不大,可是若是列表比較大則會建立一個大的臨時數據結構,用過一次後就被拋棄。使用生成器轉換數據存儲效率更高。
(生成器表達式用(),返回的是一個迭代;列表解析用的是[],返回的是一個list。)
在使用匯集函數好比min()、max()的key參數時,使用迭代更好一些。好比portfolio的例子,能夠這樣考慮:
 
1.20合併多個字典到一個單一的映射
問題:有多個字典或映射,想要邏輯結合到一塊兒變成一個映射去執行某些操做。好比查找值或者確認某些鍵是否存在
解決方案:假若有兩個字典
假如你想在兩個字典中執行查找操做(好比先在a中查找,而後a中不存在再從b中進行查找),一個簡單的方法是使用collections中的ChainMap方法。例如:
討論:ChainMap組合多個字典而且在邏輯上變成一個,可是字典沒有被真正的合併到一塊兒。ChainMap只是建立了一個字典的列表,而且重定義了列表中字典操做去掃描列表。字典大多數操做均可以使用,好比:
若是字典中有重複的key鍵,對應的值將從第一個能獲得key的字典中讀取。好比這個例子中的c['z'],將會從a字典中得到值而不是從b中得到。
改變列表中的字典元素將會影響到列表中的第一個字典(隻影響到第1個字典,不會影響到其餘字典),好比:
ChainMap和編程語言中的做用域變量一塊兒用時頗有用(好比globals,locals等)。實際上有方法可使得它更簡單:
(經過ChainMap實例的new_child()在列表中頭部增長一個新的列表子項;經過parents是得到了除了當前列表元素的其餘元素,至關於[1:]。)
做爲ChainMap的替換選擇,可能更想要的經過update()將字典合併到一塊兒,例如:
這個方法須要創一個新的字典來區分原來的字典對象(或者破壞原來的字典)。並且若是原來的字典有改變,合併後的字典不會有變化。好比:
ChainMap是用的原始的字典,因此不會有這個特性(即原始字典數據更改了,合併後的列表字典的數據也會變),好比:
相關文章
相關標籤/搜索