字典和集合的实现原理
泛映射类型
只有可散列的数据类型才能用作这些映射里的键。
如果一个对象是可散列的,那么在这个对象的生命周期中,它的散列值是不变的,而且这个对象需要实现 __hash__()
方 法。另外可散列对象还要有 __eq__()
方法,这样才能跟其他键做比较。
因此,原子不可变数据类型是可散列的。用户自定义的类型的对象一般是可散列的,散列值就是id()的返回值,但如果其实现了__eq__()
方法,并且方法中用到了这个对象的内部状态,那么只有当这些内部状态都不可变时,对象才是可散列的。
字典的常用构建方法:
>>> 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})
>>> a == b == c == d == e True
另外还有字典推导( dictcomp )式:
一些特殊的映射
defaultdict:处理找不到的键
在实例化一个 defaultdict 的时候,需要给构造方法提供 一个可调用对象,这个可调用对象会在__getitem__
碰到找不到的键的时候被调用,让__getitem__
返回某种默认值, 并且这个返回值和对应的键被对应起来。
这个用来生成默认值的可调用对象被存放在名为default_factory的实例属性中,这个属性只会在__getitem()
中被调用,在其他方法(如get)中不会发挥作用。
特殊方法__missing__
当一个类继承了dict并且实现了方法__missing__
时,在__getitem__
碰到找不到的键的时候,Python 就会自动调用它, 而不是抛出一个 KeyError 异常。
class StrKeyDict0(dict):
def __missing__(self, key):
if isinstance(key, str):
raise KeyError(key)
return self[str(key)]
def get(self, key, default=None):
try:
return self[key]
except KeyError:
return default
def __contains__(self, key):
return key in self.keys() or str(key) in self.keys()
字典的变种
- collections.OrderedDict:
这个类型在添加键的时候会保持顺序,因此键的迭代次序总是一致的。
OrderedDict 的 popitem 方法默认删除并返回的是字典里的最后一个元素,但是如果像 my_odict.popitem(last=False) 这样调用它,那么它删除并返回第一个被添加进去的元素。 - collections.ChainMap:
该类型可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。例如python解释器的变量查询规则如下:
import builtins
pylookup = ChainMap(locals(), globals(), vars(builtins))
- collections.Counter:
用于实现计数器。 - colllections.UserDict :
供用户继承编写子类。
集合
集合中的元素必须是可散列的,所以集合中不能包含集合但是可以包含frozenset。
构建集合使用{},但构建空集要使用set()。
集合的常见操作:
- 数学运算 &求交集,|求并集,-求差集,^求对称差集。
- 比较运算符:a.isdisjoint(b)判断a与b是否不相交,in判断属于,>,>=,<,<=用于判断子集,a.issubset(b),a.issuperset(b),将可迭代的b转化为集合,然后进行判断。
- 其他方法:add(e)添加元素e;discard(e)如果有,移出元素e;remove(e),移除元素e,若无抛出KeyError。
dict和set的实现原理
dict和set背后是通过散列技术实现的,因此查找的效率非常高。
这也带来了以下的一些影响:
- 键必须是可散列的:
- 支持hash函数,通过hash函数应该得到一个不变的散列值;
- 支持通过
__eq__()
来检测相等性; - 一致性:若hash(a) == hash(b)则必须要有 a == b。
- 内存开销巨大
- 键查询很快:几乎是常数时间访问
- 键的顺序取决于添加顺序;添加新键可能会改变已有键的顺序:由于散列冲突的处理
h(b)则必须要有 a == b。
2. 内存开销巨大
3. 键查询很快:几乎是常数时间访问
4. 键的顺序取决于添加顺序;添加新键可能会改变已有键的顺序:由于散列冲突的处理