7.5 字典的键
字典中的值没有任何限制。他们可以是任意Python对象,即从标准对象到用户自定义对象皆可。但是字典中的键是有类型限制的。
7.5.1 不允许一个键对应多个值
你必须明确一条原则:每个键只能对应一个项。也就是说,一键对应多个值是不允许的(像列表、元组和其他字典这样的容器对象是可以的)。当有键发生冲突(即字典键重复赋值),取最后(最近)的赋值。
Python并不会因字典中的键存在冲突而产生一个错误。它不会检查键的冲突是因为,如果真这样做的话,在每个键-值对赋值的时候都会做检查,这将会占用一定量的内存。在上面的例子里,键‘foo’被列出两次,Python从左到右检查键-值对。首先值789被赋值(给键‘foo’所对应的值),然后又很快被字符串‘xyz’替代。当给字典中一个不存在的键赋值时,键和值会被创建和添加,但如果该键已经存在(键冲突),那此键所对应的值将被替换。上面例子中,键‘foo’所对应的值被替换了两次;最后的赋值语句,值123代替了值‘xyz’。
7.5.2 键必须是可哈希的
我们在小节7.1说过,大多数Python对象可以作为键;但它们必须是可哈希的对象。像列表和字典这样的可变类型,由于它们不是可哈希的,所以不能作为键。
所有不可变的类型都是可哈希的,因此它们都可以作为字典的键。一个要说明的是问题是数字:值相等的数字表示相同的键。换句话来说,整型数字1和浮点型1.0的哈希值是相同的,即它们是相同的键。
同时,也有一些可变对象(很少)是可哈希的,它们可以做字典的键,但很少见。举一个例子,一个实现了_hash_()特殊方法的类。因为_hash_()方法返回一个整型,所以仍然是用不可变的值(做字典的键)。
为什么键必须是可哈希的?解释器调用哈希函数,根据字典中键的值来计算存储你的数据的位置。如果键是可变对象,它的值可改变。如果键发生变化,哈希函数会映像到不同的地址来存储数据。如果这样的情况发生,哈希函数就不可能可靠地存储或获取相关的数据。选择可哈希的键的原因就是因为它们的值不能改变(此问题在Python FAQ中也能找到答案)。
我们知道数字和字符串可以被用做字典的键,但元组又怎么样呢?我们知道元组是不可变的,但在小节6.17.2,我们提示过它们也可能不是一成不变的。用元组做有效的键,必须要加限制:元组中只包括像数字和字符串这样的不可变参数,才可以作为字典中有效的键。
我们用一个程序(userpw.py例7.1),来为本章关于字典的讲述做个小结。这个程序是用于管理用户名和密码的模拟登录数据系统。脚本接受新用户的信息:
这个程序管理用于登录系统的用户信息:登录名字和密码。登录用户账号建立后,已存在用户可以用登录名字和密码重返系统。新用户不能用别人的登录名建立用户账号。
例7.1
例7.2 Dictionary Example (userpw.py) (continued)
他们提供登录名和密码。账号建立后,已存在用户可用登录名和正确的密码重返系统。新用户不能用别人的登录名建立账号。
逐行解释
1 ~ 3行
在Unix初始行后,我们用一个空用户数据库初始化程序。因为我们没有把数据存储在任何地方,每次程序执行时都会新建一个用户数据库。
5 ~ 15行
newuser()函数用来建立新用户。它检查名字是否已经存在,如果证实是一个新名字,将要求用户输入他或她的密码(我们这个简单的程序没有加密),用户的密码被存储在字典里,以他们的名字做字典中的键。
17 ~ 24行
olduser()函数处理返回的用户。如果用户用正确的用户名和密码登录,打出欢迎信息。否则通知用户是无效登录并返回菜单。我们不会采用一个无限循环来提示用户输入正确的密码,因为用户可能会无意进入错误的菜单选项。
26 ~ 51行
真正控制这个脚本的是showmenu()函数,它显示给用户一个友好界面。提示信息被包括在三引号里 (”””),这样做是因为提示信息跨多行,而且比单行包含‘\n’符号的字符串更容易处理。菜单显示后,它等待用户的有效输入,然后根据菜单选项选择操作方式。try-expect语句和第6章stack.py queue.py例子里的一样(见小节6.14.1)。
53 ~ 54行
如果这个脚本被直接执行(不是通过import方式),这行代码会调用showmenu()函数运行程序。下面是我们的脚本运行结果。
7.6 集合类型
数学上,把set称做由不同的元素组成的集合,集合(set)的成员通常被称做集合元素(set elements)。 Python把这个概念引入到它的集合类型对象里。集合对象是一组无序排列的可哈希的值。是的,集合成员可以做字典中的键。数学集合转为Python的集合对象很有效,集合关系测试和union、intersection等操作符在Python里也同样如我们所预想地那样工作。
和其他容器类型一样,集合支持用in和not in操作符检查成员,由len()内建函数得到集合的基数(大小),用for循环迭代集合的成员。但是因为集合本身是无序的,你不可以为集合创建索引或执行切片(slice)操作,也没有键可用来获取集合中元素的值。
集合有两种不同的类型,可变集合(set)和不可变集合(frozenset)。如你所想,对可变集合,你可以添加和删除元素,对不可变集合则不允许这样做。请注意,可变集合不是可哈希的,因此既不能用做字典的键也不能做其他集合中的元素。不可变集合则正好相反,即,他们有哈希值,能被用做字典的键或是作为集合中的一个成员。
集合最早出现在Python2.3版本中,通过集合模块来创建,并通过ImmutableSet类和Set类进行访问。而后来,大家都认为把它们作为内建的数据类型是个更好的主意,因此这些类被用C重写改进后包含进Python2.4。关于集合类型和这些类改进的更多内容,可阅读此文获得详情:PEP 218,链接地址:http://python.org/peps/pep-0218.html。
虽然现在集合类型已经是Python的基本数据类型了,但它经常会以用户自定义类的形式出现在各种Python程序中,就像复数一样(复数从Python1.4版本起成为python的一个数据类型),这样重复的劳动已数不胜数了。在现在的Python版本之前,(即使集合类型对许多人的程序来说并不是最理想的数据结构,)许多人仍然试图给列表和字典这样的Python标准类型添加集合功能,这样可以把它们作为真正集合类型的代理来使用。因此现在的使用者有包括“真正”集合类型在内的多种选择。
在我们详细讲述Python的集合对象之前,我们必须理解Python中的一些数学符号(见表7.3),这样对术语和功能有一个清晰的了解。
7.6.1 如何创建集合类型和给集合赋值
集合与列表([])和字典({})不同,没有特别的语法格式。列表和字典可以分别用他们自己的工厂方法list()和dict()创建,这也是集合被创建的唯一方法——用集合的工厂方法set()和 frozenset():
7.6.2 如何访问集合中的值
可以遍历查看集合成员或检查某项元素是否是一个集合中的成员。
7.6.3 如何更新集合
用各种集合内建的方法和操作符添加和删除集合的成员。
我们之前提到过,只有可变集合能被修改。试图修改不可变集合会引发异常。
7.6.4 如何删除集合中的成员和集合
前面我们看到如何删除集合成员。如果如何删除集合本身,可以像删除任何Python对象一样,令集合超出它的作用范围,或调用del将他们直接清除出当前的名称空间。如果它的引用计数为零,也会被标记以便被垃圾回收。