字典
字典包括一系列的索引,不过就已经不叫索引了,而是叫键(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 #可见输出是无序的
还有一种办法遍历键与键值:
def print_hist(h):
for (key,key_value) in h.items():
#会遍历所有对应的键与键值。
字典有一个内置的叫做 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))