Python字典和集合拾遗 --流畅的python
字典和集合
本章对python中字典和集合的内容做一些拾遗,主要内容来自流畅的python第三章。
建议通过Jupyter Notebook来试验本文代码,关于Jupyter Notebook使用可以阅读这里, 本文代码已经放在这里。
本章内容适合对python有一定了解的读者。
通过本章你可以了解到:
- 字典和集合的基本使用
- 如何处理字典中不存在的键
- 字典和集合为何比列表高效
- 一些简单字典变种的介绍
映射
我们知道,字典的本质是键(key)和值(value)的映射,在python中,除了字典之前还有一些其他的特殊映射类型,但是它们都是基于collections.abc.Mapping
的。
可散列
需要注意的是,这些映射类型对key是有要求的,即只有可散列的数据类型才能用作映射的键值。那么什么是可散列的数据类型呢:
- 从定义上来说,任何实现了
__hash__()
和__qe__()
的对象都是可散列的,前者将key值映射到另一个空间,后者则用来比较两个键值是否相同。 - 具体而言,所有原子不可变的数据类型都是可散列的(str,bytes和数值),frozenset也可散列。
- 元组(tuple)有些特殊,当且仅当元祖内所有元素都可散列时,它才是可散列的。
- 列表(list)是不可散列的。
下面我们通过两个元组的例子来说明一下:
1 | hash((1,2,3)) |
2528502973977326415
1 | hash((1,2,[3])) |
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-2-f5fc4ae74995> in <module>()
----> 1 hash((1,2,[3]))
2 # 这里由于元组中包含了不可散列的列表元素[3],因此无法求其hash
TypeError: unhashable type: 'list'
消失的键
映射类型(除了dict
,常用的映射类型还有defaultdict
和OrderedDict
)的方法很丰富,大部分都较为常用,下面介绍一个较为冷门但很有用的方法setdefault
。
这个方法可以用来处理找不到的键值。对于一个字典d
及其键k
,我们通常用d[k]
来获取值,但是如果字典中不包含该键时会报KeyError
,当然我们可以通过d.get(k, default)
来给找不到的键指定默认值。
不过setdefault
往往会更方便,相比d.get(k, default)
,它加了一个当d
中不含k
时设定d[k]=default
的动作。
举一个例子,下面三种写法是等价的:
1 | key = 'foo' |
当然,另一种常用的方法是使用defaultdict
类,或是自己定义一个dict
的子类并实现__missing__
方法,前一种方法简单查一下defaultdict
的用法就行,不再赘述,主要来谈谈__missing__
的实现。
所有映射类型在处理找不到的键时,都会用到这个方法,具体地说,当该类执行__getitem__
时找不到该键,就会自动调用__missing__
方法,需要注意的是默认来说,get
和__contains__
并不会使用__missing__
。
例子 下面举一个例子,我们现在有一个字典文件,其中的key都是一些数字,但是可惜的是这些key有的是以int
存在的,有的则是以字符串存在的。我们的需要保证,对这个字典执行查找时,数字key的形式不会造成差异,即123
和"123"
作为查询的键值应该是等价的。
1 | d = dict([('123', 'Yes'), (234, 'No')]) |
Yes
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-4-80e778fd0b83> in <module>()
1 d = dict([('123', 'Yes'), (234, 'No')])
2 print(d['123'])
----> 3 print(d[123])
4 # 可以发现查询123时出现了KeyError
KeyError: 123
1 | # 下面我们自己定义一个dict,重新实现__missing__方法来满足上面的需求 |
Yes
Yes
No
No
字典变种
除了defaultdict
之外,collections
包中还提供了一些其他的常用字典的变种。
- OrderedDict 该类型在添加键时会保持顺序,因此键之间的顺序是固定的。
- ChainMap 该类型可以容纳数个不同的映射对象并将其合并为一个映射对象
- Counter 这个类型给键值对应了一个整数计数器
不可变映射
有时我们有这样的需求,即设置映射为只读使其不可更改。例如对于一条学生数据,其名字key为name,不可更改。
此时我们可以用types
模块中的MappingProxyType
来完成这个需求。通过该类来构造原映射A,则会返回一个只读的映射视图,该视图是不可更改的,但是任何对原映射A的更改也会反映到这个映射视图上。
举例来说:
1 | from types import MappingProxyType |
映射视图: {'name': 'John'}
1 | #下面我们试图来修改这个映射视图,会发现报错 |
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-7-b70293becf62> in <module>()
1 #下面我们试图来修改这个映射视图,会发现报错
----> 2 std_proxy['grade'] = 100
TypeError: 'mappingproxy' object does not support item assignment
1 | # 然后我们修改原映射 |
可以看到映射视图也随之改变: {'name': 'John', 'grade': 100}
grade 100
集合
集合本质上许多唯一元素的无序聚集,当然数学意义上的很多集合运算python也都用相应实现。
a | b
返回两个集合的并集a & b
返回两个集合的交集a - b
返回两个集合的差集i in a
元素i
是否在集合a
中b <= a
集合b
是否为a
的子集b < a
集合b
是否为a
的真子集
除了以上二元中缀运算,还有一些其他常见方法:
a.add(i)
添加元素i
a.clear()
清除所有元素a.copy()
对a进行浅复制a.discard(i)
如果a
中有i
这个元素的话,就将它移除a.pop()
返回a
中的一个元素并将其从集合中移除,若a
为空,则抛出异常a.remove(i)
从a
中移除i
,若不含该元素,抛出异常
set和dict背后
掌握了基本的使用时候,我们对set和dict背后的知识再做一些探索。
首先实验不难发现,相对于list来说,set和dict是非常高效的。究其原因,以在这些对象中寻找元素为例,set和dict用的都是哈希算法也就是散列表算法来查找(关于哈希算法可以看维基百科的介绍),此时复杂度为哈希函数复杂度+冲突处理复杂度,一般是一个很小的函数,而list的复杂度则是线性的。
当然,这样的处理有优势也有限制:
- 速度快,效率高
- 键值必须是可散列的
- dict的内存开销巨大(因为散列表通常是稀疏的)
- 键的次序取决于添加顺序(同样的键在两个dict里可能因为添加顺序不同而位置不同)
- 将字典里加入新键可能会改变已有键的顺序(加入新键引发扩容,此时可能引起散列表中其他键值的次序变化)
因此,可以看到,除了内存开销的问题没有很好的解决方法,顺序变化的限制可以通过使用字典变种OrderedDict
来解决。