dict - Python里的"智能电话簿"
Python有个很实用的内置功能叫dict(dictionary的缩写),你可以把它想象成一本智能电话簿。
别的编程语言里也有类似的东西,例如Java,叫map。
它的工作原理很简单:用键(key)来存储和查找值(value),速度快得惊人。
来看个实际场景。假如你要记录班上同学的成绩,用传统的list来做会是这样:
names = ['Michael', 'Bob', 'Tracy']
scores = [95, 75, 85]
想知道某个同学的成绩?得先在names里找到他的位置,再去scores里取对应的分数。这还只是三个人,要是有一百个、一千个同学呢?查找时间会越来越长,简直是个噩梦。
dict就不一样了。它直接建立"姓名"和"成绩"的对应关系:
>>> d = {'Michael': 95, 'Bob': 75, 'Tracy': 85}
>>> d['Michael']
95
为什么dict能这么快?道理其实跟我们查纸质字典一样。
想象一下,你手里有本一万字的汉语字典,要查某个字。笨办法是从第一页开始一页页翻,直到找到为止——这就是list的查找方式,字典越厚越费时间。
聪明的做法呢?先看部首索引,找到对应的页码,直接翻过去。不管查哪个字,速度都差不多,这就是dict的工作方式。
dict内部会根据key(比如’Michael’)直接计算出value(95)存放的内存地址,然后一步到位取出来。这种通过key计算存储位置的算法,有个专业名字叫哈希算法(Hash)。
往dict里添加数据很灵活。除了初始化时就设定好,还可以随时添加:
>>> d['Adam'] = 67
>>> d['Adam']
67
有个特点需要注意:每个key只能对应一个value。如果你给同一个key赋值多次,后面的会覆盖前面的:
>>> d['Jack'] = 90
>>> d['Jack']
90
>>> d['Jack'] = 88
>>> d['Jack']
88
访问不存在的key会报错:
>>> d['Thomas']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'Thomas'
怎么避免这种尴尬?有两个办法。一个是用in先确认key存不存在:
>>> 'Thomas' in d
False
另一个是用dict自带的get()方法,找不到key时它会返回None,或者返回你指定的默认值:
>>> d.get('Thomas')
>>> d.get('Thomas', -1)
-1
(Python交互环境下,None不会显示出来,这是正常的)
删除某个key用pop(key),对应的value也会一起消失:
>>> d.pop('Bob')
75
>>> d
{'Michael': 95, 'Tracy': 85}
有件事可能会让你意外:dict里元素的顺序跟你添加它们的顺序没什么关系。
dict和list各有各的特点。dict查找和插入超快,不管数据量多大速度都很稳定,但代价是占用内存比较多。list正好相反,查找和插入会随着数据量增加而变慢,不过它省内存。这就是典型的"用空间换时间"。
Python代码里到处都能看到dict的身影,用好它很关键。有个铁律必须记住:dict的key必须是不可变对象。
为什么?因为dict要根据key来计算存储位置,如果同一个key每次计算出来的结果都不一样,整个系统就乱套了。在Python里,字符串、整数这些都是不可变的,放心拿来当key。但list是可变的,就不行:
>>> key = [1, 2, 3]
>>> d[key] = 'a list'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
set - key的集合体
set跟dict是近亲,它也是一组key的集合,只不过不存value。既然key不能重复,set里自然也不会有重复元素。
创建set很简单,直接用花括号把元素列出来:
>>> s = {1, 2, 3}
>>> s
{1, 2, 3}
或者把一个list转换成set:
>>> s = set([1, 2, 3])
>>> s
{1, 2, 3}
注意看,传进去的[1, 2, 3]是list(方括号),显示出来的{1, 2, 3}是set(花括号)。这个显示顺序并不代表set内部是有序的。
重复的元素会被自动过滤掉:
>>> s = {1, 1, 2, 2, 3, 3}
>>> s
{1, 2, 3}
用add(key)可以往set里加元素。重复添加同一个元素不会报错,只是没有效果而已:
>>> s.add(4)
>>> s
{1, 2, 3, 4}
>>> s.add(4)
>>> s
{1, 2, 3, 4}
用remove(key)删除元素:
>>> s.remove(4)
>>> s
{1, 2, 3}
set就是数学意义上的集合:无序、不重复。两个set可以做交集、并集这些数学运算:
>>> s1 = {1, 2, 3}
>>> s2 = {2, 3, 4}
>>> s1 & s2
{2, 3}
>>> s1 | s2
{1, 2, 3, 4}
set和dict的唯一区别就是不存value,但实现原理是一样的。这意味着set也不能放可变对象,因为没法判断两个可变对象是不是真的相等,就没法保证"不重复"这个特性。你可以试试把list放进set,看看会发生什么。
关于不可变对象的深入理解
前面说过,str是不可变对象,list是可变对象。
对list操作时,它的内容会真的改变:
>>> a = ['c', 'b', 'a']
>>> a.sort()
>>> a
['a', 'b', 'c']
但对str操作呢:
>>> a = 'abc'
>>> a.replace('a', 'A')
'Abc'
>>> a
'abc'
咦,replace()确实生成了'Abc',但变量a还是'abc'?怎么回事?
我们换个写法就清楚了:
>>> a = 'abc'
>>> b = a.replace('a', 'A')
>>> b
'Abc'
>>> a
'abc'
关键要理解:a只是个变量名,真正的字符串对象是'abc'。我们常说"对象a的内容是’abc’",其实是指变量a指向了内容为’abc’的字符串对象:
┌───┐ ┌───────┐
│ a │────▶│ 'abc' │
└───┘ └───────┘
当你调用a.replace('a', 'A')时,实际上是在字符串对象'abc'上调用replace方法。虽然这方法叫"replace",但它并没有改变'abc'本身。它做的是创建一个新字符串'Abc'然后返回。如果我们用变量b接住这个新字符串,就很好理解了:变量a还指向原来的'abc',变量b指向新的'Abc':
┌───┐ ┌───────┐
│ a │────▶│ 'abc' │
└───┘ └───────┘
┌───┐ ┌───────┐
│ b │────▶│ 'Abc' │
└───┘ └───────┘
这就是不可变对象的特性:不管你调用什么方法,对象本身的内容永远不会变。这些方法只会创建并返回新对象,原对象始终保持不变。
小结
dict这种key-value存储结构在Python里用处大得很,记住要用不可变对象做key,其中字符串是最常见的选择。
tuple虽然也是不可变对象,但你可以试试把(1, 2, 3)和(1, [2, 3])分别放进dict或set,看看会有什么结果,想想为什么会这样。