【練習題】第十一章--字典(Think python)
字典
字典包括一系列的索引,不過就已經不叫索引了,而是叫鍵(Key),然後還對應著一個個值,就叫鍵值(Key Value)。每個鍵對應著各自的一個單獨的鍵值。這種鍵和鍵值的對應關係也叫鍵值對,有時候也叫項。
這種輸出的格式也可以用來輸入。比如你可以這樣建立一個有三個項的字典:
>>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
再來輸出一下,你就能看到字典建好了,但順序不一樣:
>>> eng2sp {'one': 'uno', 'three': 'tres', 'two': 'dos'}
in 運算子也適用於字典;你可以用它來判斷某個鍵是不是存在於字典中(是判斷鍵,不能判斷鍵值)。
>>> 'one' in eng2sp
True
>>> 'uno' in eng2sp
False
要判斷鍵值是否在字典中,你就要用到 values 方法,這個方法會把鍵值返回,然後用 in 判斷就可以了:
>>> vals = eng2sp.values()
>>> 'uno' in vals
True
in 運算子在字典中和列表中有不同的演算法了。對列表來說,它就按照順序搜尋列表中的每一個元素,隨著列表越來越長了,這種搜尋就消耗更多的時間,才能找到正確的位置。
而對字典來說,Python 使用了一種叫做雜湊表的演算法,這就有一種很厲害的特性:in 運算子在對字典來使用的時候無論字典規模多大,無論裡面的項有多少個,花費的時間都是基本一樣的。
1.用字典作為計數器
假設你得到一個字串,然後你想要查一下每個字母出現了多少次。你可以通過一下方法來實現:
1.建立26個變數。2.建立一個有26個元素的列表。3.建立一個字典,以字母為鍵,出現頻數為鍵值。
實現是一種運算進行的方式;有的實現要比其他的更好一些。比如用字典來實現的優勢就是我們不需要實現知道字串中有哪些字母,只需要為其中存在的字母來提供儲存空間。
下面是程式碼樣例:
def histogram(s): d = dict() for c in s: if c not in d: d[c] = 1 else: d[c] += 1 return d
字典有一個方法,叫做 get,接收一個鍵和一個預設值。如果這個鍵在字典中存在,get 就會返回對應的鍵值;如果不存在,它就會返回這個預設值。比如:
>>> h = histogram('a')
>>> h
{'a': 1}
>>> h.get('a', 0)
1
>>> h.get('b', 0)
0
然後由這個get方法重寫histpgram():
def histogram(s):
d=dict()
for c in s:
d[c]=d.get(c,0)+1
2.迴圈與字典
如果你在 for 語句裡面用字典,程式會遍歷字典中的所有鍵。例如下面這個 print_hist 函式就輸出其中的每一個鍵與對應的鍵值:
def print_hist(h):
for c in h:
print(c, h[c])
>>> h = histogram('parrot')
>>> print_hist(h)
a 1 p 1 r 2 t 1 o 1 #可見輸出是無序的
字典有一個內建的叫做 keys 的方法,返回字典中的所有鍵成一個列表,以不確定的順序。
修改上面程式以輸出有順序的鍵:
def print_hist(h):
t=h.keys()
t.sort()
for c in t:
print(c,h[c])
3.逆向查詢
沒有一種簡單的語法能實現這樣一種逆向查詢;你必須搜尋一下。
def reverse_lookup(d, v):
for k in d:
if d[k] == v:
return k
raise LookupError()
這個函式是搜尋模式的另一個例子,用到了一個新的功能:raise。raise語句會導致一個異常;在這種情況下是 LookupError,這是一個內建異常,表示查詢操作失敗。
如果我們運行了整個迴圈,就意味著 v 在字典中沒有作為鍵值出現果,所以就 raise 一個異常回去。
下面是一個成功進行逆向查詢的樣例:
>>> h = histogram('parrot')
>>> k = reverse_lookup(h, 2)
>>> k
'r'
下面這個是一個不成功的:
>>> k = reverse_lookup(h, 3)
Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in reverse_lookup ValueError
你自己 raise 一個異常的效果就和 Python 丟擲的異常是一樣的:程式會輸出一個追溯以及一個錯誤資訊。
raise 語句可以給出詳細的錯誤資訊作為可選的引數。如下所示:
>>> raise ValueError('value does not appear in the dictionary')
Traceback (most recent call last): File "<stdin>", line 1, in ?
ValueError: value does not appear in the dictionary
逆向查詢要比正常查詢慢很多很多;如果要經常用到的話,或者字典變得很大了,程式的效能就會大打折扣。
4.字典和列表
字典中的鍵值可組成列表,而不能用鍵。因為字典是用雜湊表(散列表)來實現的,這就意味著所有鍵都必須是雜湊的。
下面是一個逆轉字典的函式:
def invert_dict(d):
inverse = dict()
for key in d:
val = d[key]
if val not in inverse:
inverse[val] = [key]
else:
inverse[val].append(key)
return inverse
>>> hist = histogram('parrot')
>>> hist
{'a': 1, 'p': 1, 'r': 2, 't': 1, 'o': 1}
>>> inverse = invert_dict(hist)
>>> inverse
{1: ['a', 'p', 't', 'o'], 2: ['r']}
hash 是一個函式,接收任意一種值,然後返回一個整數。字典用這些整數來儲存和查詢鍵值對,這些整數也叫做雜湊值。
如果鍵不可修改,系統工作正常。但如果鍵可以修改,比如是列表,就悲劇了。例如,你建立一個鍵值對的時候,Python 計算鍵的雜湊值,然後存在相應的位置。如果你修改了鍵,然後在計算雜湊值,就不會指向同一個位置了。這時候一個鍵就可以有兩個指向了,或者你就可能找不到某個鍵了。總之字典都不能正常工作了。
這就是為什麼這些鍵必須是雜湊的,而像列表這樣的可變型別就不行。解決這個問題的最簡單方式就是使用元組,這個我們會在下一章來學習。
因為字典是可以修改的,所以不能用來做鍵,只能用來做鍵值。
5.Memos備忘
另外一種思路就是儲存一下已經被計算過的值,然後儲存在一個字典中。之前計算過的值儲存起來,這樣後續的運算中能夠使用,這就叫備忘。下面是一個用這種思路來實現的斐波那契函式:
known = {0:0, 1:1}
def fibonacci(n):
if n in known:
return known[n]
res = fibonacci(n-1) + fibonacci(n-2)
known[n] = res
return res
known 是一個用來儲存已經計算斐波那契函式值的字典。開始專案有兩個,0對應0,1對應1,各自分別是各自的斐波那契函式值。
這樣只要斐波那契函式被呼叫了,就會檢查 known 這個字典,如果裡面有計算過的可用結果,就立即返回。不然的話就計算出新的值,並且存到字典裡面,然後返回這個新計算的值。
如果你執行這一個版本的斐波那契函式,你會發現比原來那個版本要快得多。
6.全域性變數
要在函式內部來給全域性變數重新賦值,必須要在使用之前宣告這個全域性變數(不然就只是一個區域性變數):
been_called = False
def example2():
global been_called
been_called = True
global 那句程式碼的效果是告訴直譯器:『在這個函式內,been_called 使之全域性變數;不要建立一個同名的區域性變數。』
如果全域性變數指向的是一個可修改的值,你可以無需宣告該變數就直接修改:
known = {0:0, 1:1}
def example4():
known[2] = 1
所以你可以在全域性的列表或者字典裡面新增、刪除或者替換元素,但如果你要重新給這個全域性變數賦值,就必須要聲明瞭:
def example5():
global known
known = dict()
全域性變數很有用,但不能濫用,要是總修改全域性變數的值,就讓程式很難除錯了。
7.除錯
現在資料結構逐漸複雜了,再用列印輸出和手動檢驗的方法來除錯就很費勁了。下面是一些對這種複雜資料結構下的建議:
縮減輸入:儘可能縮小資料的規模。如果程式要讀取一個文字文件,而只讀前面的十行,或者用你能找到的最小規模的樣例。你可以編輯一下檔案本身,或者直接修改程式來僅讀取前面的 n 行,這樣更好。如果存在錯誤了,你可以減小一下 n,一直到錯誤存在的最小的 n 值,然後再逐漸增加 n,這樣就能找到錯誤並改正了。
檢查概要和型別:這回咱就不再列印檢查整個資料表,而是列印輸出資料的概要:比如字典中的項的個數,或者一個列表中的數目總和。導致執行錯誤的一種常見原因就是型別錯誤。對這類錯誤進行除錯,輸出一下值的型別就可以了。
寫自檢程式碼:有時你也可以寫自動檢查錯誤的程式碼。舉例來說,假如你計算一個列表中數字的平均值,你可以檢查一下結果是不是比列表中的最大值還大或者比最小值還小。這也叫『心智檢查』,因為是來檢查結果是否『瘋了』(譯者注:也就是錯得很荒誕的意思。)另外一種檢查方法是用兩種不同運算,然後對比結果,看看他們是否一致。後面這種叫『一致性檢查』。
格式化輸出:格式化的除錯輸出,更容易找到錯誤。在6.9的時候我們見過一個例子了。pprint 模組內建了一個 pprint 函式,該函式能夠把內建的型別用人讀起來更容易的格式來顯示出來(pprint 就是『pretty print』的縮寫)。
再次強調一下,搭建腳手架程式碼的時間越長,用來除錯的時間就會相應地縮短。
8.術語列表
mapping: A relationship in which each element of one set corresponds to an element of another set.
對映:一組資料中元素與另一組資料中元素的一一對應的關係。
implementation: A way of performing a computation.
實現:進行計算的一種方式。
練習1:
寫一個函式來讀取 words.txt 檔案中的單詞,然後作為鍵存到一個字典中。鍵值是什麼不要緊。然後用 in 運算子來快速檢查一個字串是否在字典中。
如果你做過第十章的練習,你可以對比一下這種實現和列表中的 in 運算子以及對摺搜尋的速度。
mycode:
def read_dict(filename):
d=dict()
fin=open(filename)
i=0
for word in fin:
i+=1
d[word]=i
return d
def is_indict(s,d):
if s in d:
return True
else:
return False
練習2:
讀一下字典中 setdefault 方法的相關文件,然後用這個方法來寫一個更精簡版本的 invert_dict 函式。
def invert_dict(d):
inverse=dict()
for key in d:
val=d[key]
if inverse.setdefault(val,key)!=key:
inverse[val].append(key)
return inverse
練習3:
用備忘的方法來改進一下第二章練習中的Ackermann函式,看看是不是能讓讓函式處理更大的引數。提示:不行。
cache = {}
def ackermann(m, n):
"""Computes the Ackermann function A(m, n)
See http://en.wikipedia.org/wiki/Ackermann_function
n, m: non-negative integers
"""
if m == 0:
return n+1
if n == 0:
return ackermann(m-1, 1)
if (m, n) in cache:
return cache[m, n]
else:
cache[m, n] = ackermann(m-1, ackermann(m, n-1))
return cache[m, n]
練習4:
如果你做過了第七章的練習,應該已經寫過一個名叫 has_duplicates 的函數了,這個函式用列表做引數,如果裡面有元素出現了重複,就返回真。
用字典來寫一個更快速更簡單的版本。
def has_duplicates(t):
"""Checks whether any element appears more than once in a sequence.
Simple version using a for loop.
t: sequence
"""
d = {}
for x in t:
if x in d:
return True
d[x] = True
return False
def has_duplicates2(t):
"""Checks whether any element appears more than once in a sequence.
Faster version using a set.
t: sequence
"""
return len(set(t)) < len(t)#set() 函式建立一個無序不重複元素集,可進行關係測試,刪除重複資料,還可以計算交集、差集、並集等。
練習5:
一個詞如果翻轉順序成為另外一個詞,這兩個詞就為『翻轉詞對』(參見第五章練習的 rotate_word,譯者注:作者這個練習我沒找到。。。)。
寫一個函式讀取一個單詞表,然後找到所有這樣的單詞對。
def rotate_word(txt):
fin=open(txt)
t=[]
for word in fin:
t.append((word.strip())[::-1])
return t
def read_file(txt):
fin=open(txt)
d=dict()
for word in fin:
d[word.strip()]=1
return d
def is_rotate(t,d):
print('rotational word is:')
for word in t:
if word in d:
print(word,word[::-1])
txt='words.txt'
is_rotate(rotate_word(txt),read_file(txt))