第三章 字典与集合
文章目录
字典这个数据结构活跃在所有Python程序的背后,即便你的源码里并没有直接用到它。
dict类型不但在各种程序里广泛使用,它也是Python语言的基石。模块的命名空间、实例的属性和函数的关键字参数中都可以看到字典的身影。跟它有关的内置函数都在__builtins__.__dict__模块中。
3.1 泛映射类型
collections.abc模块中有Mapping和MutableMapping这两个抽象基类,定义了构建一个映射类型所需要的最基本的接口,下图是一些常用的方法:
内置数据类型dict和其他类似的类型均使用这些接口:
from collections import abc
my_dict = {}
isinstance(my_dict, abc.Mapping)
# True
不过一般的的映射类型不会直接继承这两个抽象基类,而是依托于dict或是collections.UserDict进行扩展。例如标准库里的所有映射类型都是利用dict来实现的,因此它们有个共同的限制,即只有可散列的数据类型才能用作这些映射里的键。
可能当你学字典的时候,就知道键必须是不可变的,如字符串,数字或元组。事实上,这个说法并不准确,准确来说,键必须是可散列数据。可散列数据的定义为:
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现__hash__()方法。
换句话说,这个对象必须是可哈希的:原子不可变数据类型(str、bytes和数值类型)都可哈希,都是可散列类型。元组的话,只有当一个元组包含的所有元素都是可散列类型的情况下,它才是可散列的,具体原因可以参见我的另一篇博文:元组的相对不可变性。
>>> hash(2)
2
>>> hash('fr')
7645933407608567419
>>> hash((2,3))
3713082714463740756
>>> hash((2,[3,4]))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
另外可散列对象还要有__eq__()方法,这样才能跟其他键做比较。如果两个可散列对象是相等的,那么它们的散列值一定是一样的……
一般来讲用户自定义的类型的对象都是可散列的,散列值就是它们的id( )函数的返回值,所以所有这些对象在比较的时候都是不相等的。
3.2 字典推导
字典的创建方式太多了,下面这些方式都可以创建字典:
>>> a = dict(one=1, two=2, three=3)
>>> b = {'one': 1, 'two': 2, 'three': 3}
>>> c = dict(zip(['one', 'two', 'three'], [1, 2, 3]))
>>> d = dict([('two', 2), ('one', 1), ('three', 3)])
>>> e = dict({'three': 3, 'one': 1, 'two': 2})
我们在这里讲一种新的创建方式字典推导,可以从任何以键值对作为元素的可迭代对象中构建出字典。
keys = 'one two three'.split(' ')
values = range(1, 4)
f = {key: value for key,value in zip(keys,values)}
如果你会使用列表推导式,上面的例子很容易理解,如果现在还不知道这个功能,我想是时候掌握他了!
3.3 常见的映射方法
映射类型有很多,不过像前面所说的,大多数都依靠dict扩展,比如collections模块内的defaultdict和OrderedDict。所以大多数的方法都是大同小异,就像d.items()获取多有键值对,几乎是所有影射对象共有的,再比如in操作符所对应的魔法方法__contains__。大多数方法相信大家在初学python中都知道,在这里我们额外介绍几个常用方法:
当字典d[k]不能找到正确的键的时候,Python会抛出KeyError异常,所以几乎每个Python程序员都知道可以用d.get(k, default)来代替d[k],给找不到的键一个默认的返回值:
d = dict(one=1, two=2, three=3)
d.get('four','N/A')
这样很好,但是如果我们要更新某个键对应的值的时候,你可能会想到用:
if key not in my_dict:
my_dict[key] = []
my_dict[key].append(new_value)
我在找工作笔试的时候,不止一次用过这个代码,注意,我们这里所说的是增删的更新,而不是修改,如果是修改的话,你大可以直接使用my_dict[key] = new_value直接进行。
上面的代码看起来并没有什么问题,直到我看了这本书,我才发现更为优雅的做法是像下面这样:
my_dict.setdefault(key, []).append(new_value)
尽管两者的效果是一样的,但是前者至少要进行两次键查询(如果键不存在的话,就是三次),用setdefault只需要一次就可以完成整个操作。
也就是说,大家如果只想获取某个key对应的value,可以使用d.get(k, default);如果只想修改,不关注其存在不存在,直接使用my_dict[key] = new_value;如果只想在键存在的情况进行增删的更新,推荐使用setdefault(key, [])方法。
所有的映射类型在处理找不到的键的时候,都会牵扯到__missing__方法。只有在__getitem__碰到找不到的键的时候,Python就会自动调用它,而不是抛出一个KeyError异常。不过大多数时候我们并用不到,我们只用一个例子简单地说明。
# 当有非字符串的键被查找的时候,Mydict将该键转换为字符串再次尝试
class Mydict(dict):
def __missing__(self, key):
# 如果是个整数,就再给一次机会,否则就报错
if isinstance(key, int):
return self[str(key)]
else:
raise KeyError
def get(self, key):
try:
return self[key]
except KeyError:
return None
def __contains__(self, key):
return key in self.keys() or str(key) in self.keys()
d = Mydict(zip(['1', '2', '4'], 'one two four'.split(' ')))
# {'1': 'one', '2': 'two', '4': 'four'}
>>>2 in d
True
>>>d[4]
'four'
在上面的例子中,我们使用get方法把查找工作用self[key]的形式委托给__getitem__,这样在宣布查找失败之前,还能通过__missing__再给某个键一个机会。比如说d[4],当未查询到是时,__getitem__会调用__missing__方法,再次进行查找,最终返回d[‘4’]。是我们从dict继承到的__contains__方法不会在找不到键的时候调用__missing__方法,在这里列出来只是为了保证程序的一致性。
3.4 字典的变种
collections标准库里面还有很多其他的映射类型,我们现在大致看一下,不是很常用到,仅作了解
类型 | 介绍 |
---|---|
collections.OrderedDict | 在添加键的时候会保持顺序,因此键的迭代次序总是一致的。OrderedDict的popitem方法默认删除并返回的是字典里的最后一个元素,但是如果像my_odict.popitem(last=False)这样调用它,那么它删除并返回第一个被添加进去的元素。 |
collections.ChainMap | 可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。这个功能在给有嵌套作用域的语言做解释器的时候很有用,可以用一个映射对象来代表一个作用域的上下文。 |
collections.Counter | 这个映射类型会给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数,或者是当成多重集来用——多重集合就是集合里的元素可以出现不止一次。Counter实现了+和-运算符用来合并记录,还有像most_common([n])这类很有用的方法。most_common([n])会按照次序返回映射里最常见的n个键和它们的计数。 |
collections.UserDict | 这个类其实就是把标准dict用纯Python又实现了一遍,是专门让用户继承写子类的。更倾向于从UserDict而不是从dict继承的主要原因是,dict有时会在某些方法的实现上走一些捷径,导致我们不得不在它的子类中重写这些方 |
- collections.Counter
import collections
# 对可散列对象计数
ct = collections.Counter('aasddwwddafgsgdfsg')
>>>ct
Counter({'a': 3, 's': 3, 'd': 5, 'w': 2, 'f': 2, 'g': 3})
# 增加对象
ct.update('aaassffff')
>>>ct
Counter({'a': 6, 's': 5, 'd': 5, 'w': 2, 'f': 6, 'g': 3})
# 最多的n个键
ct.most_common(2)
[('a', 6), ('f', 6)]
——未完待续——
欢迎关注我的微信公众号