概览
在collections.abc中定义了Mapping和MutableMapping去定义dict和类似类型的接口。一般的特殊类型的映射(mappings)都是继承自dict或者collections.UserDict。
Hashable
所有在标准库里的映射类型都是继承自dict,所以他们都有一个限制,key必须是hashable的。
一个对象是hashable的有三点:
1.它有一个hash值在整个生命周期内都是不变的(需要一个__hash__()
方法)。
2.能够和其他对象比较(需要一个__eq__()
方法)
3.hashable的对象如果比较是相等的必须有相同的hash值。
常见的hashable的类型:
1.原子的不可变类型(immutable)都是hashable的,比如str, bytes, numeric类型。
2.frozenset总是hashable的,因为它的元素要求必须都是hashable的。
3.tuple如果所有的元素都是hashable的就是hashable的。
4.用户自定义类型缺省是hashable的,因为他们的hash值就是他们的id()并且是不相等的。如果一个对象实现了自定义__eq__
方法,就需要考虑到这个对象的内部状态,只有当所有的属性是immutable的这个对象才是hashable的。
>>> tt = (1, 2, (30, 40))
>>> hash(tt)
8027212646858338501
>>> tl = (1, 2, [30, 40])
>>> hash(tl)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'list'
>>> class A:
... def __init__(self):
... pass
...
>>> a = A()
>>> b = A()
>>> hash(a)
-9223372036571095159
>>> hash(b)
283680646
>>> a == b
False
关于__hash__()
和__eq__
的自定义这里还有些特殊的要求,详细可以看python的手册中相关的内容。
这里还引申出另一个问题就是is
和==
的区别。is
比较的是对象的id
是否相同,意义在于是否是同一个实例对象,是否是同一个内存地址。而==
比较的是两个对象的内容是否相同,默认会调用对象的__eq__
方法。继承自 object 的__eq__
方法比较两个对象的id,结果与 is 一样。但是多数Python的对象会覆盖object的__eq__
方法,而定义内容的相关比较,所以比较的是对象属性的值。
>>> a = [1, 2, 3]
>>> b = a
>>> b is a
True
>>> b == a
True
>>> b = a[:]
>>> b is a
False
>>> b == a
True
数字类型用is
比较有一些特殊,对于[-5, 256]区间内的small_ints,python会进行缓存,不会创建新的对象。
>>> a = 256
>>> b = 256
>>> a is b
True
>>> a == b
True
>>> a = 257
>>> b = 257
>>> a is b
False
>>> a == b
True
字符串使用is
的结果也不一定是相同。
>>> c = 'abc.com'
>>> b = 'abc.com'
>>> c is b
False
>>> c == b
True
>>> c = 'efg'
>>> d = 'efg'
>>> c is d
True
>>> c == d
True
tuple,list,set的is
和==
的结果都不一样。
>>> a = (1, 2, 3)
>>> b = (1, 2, 3)
>>> a == b
True
>>> a is b
False
>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> a == b
True
>>> a is b
False
>>> a = set([1, 2, 3])
>>> b = set([1, 2, 3])
>>> a == b
True
>>> a is b
False
dict Comprehensions
创建dict有如下几种方式。
>>> 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
>>> DIAL_CODES = [
... (86, 'China'),
... (91, 'India'),
... (81, 'Japan'),
... ]
>>> country_code = {country: code for code, country in DIAL_CODES}
>>> country_code
{'China': 86, 'India': 91, 'Japan': 81}
处理缺失值
同一个逻辑的几种实现方式。
>>> my_dict = {}
>>> key = 'a'
>>> if key not in my_dict:
... my_dict[key] = []
...
>>> my_dict[key].append("value")
>>> my_dict
{'a': ['value']}
>>> my_dict = {}
>>> key = 'a'
>>> my_list = my_dict.get(key, [])
>>> my_list.append("value")
>>> my_dict[key] = my_list
>>> my_dict
{'a': ['value']}
>>> my_dict = {}
>>> key = 'a'
>>> my_list = my_dict.setdefault(key, [])
>>> my_list.append("value")
>>> my_dict
{'a': ['value']}
>>> my_dict = collections.defaultdict(list)
>>> key = 'a'
>>> my_dict[key].append("value")
>>> my_dict
defaultdict(<class 'list'>, {'a': ['value']})
>>> class ListDict(dict):
... def __missing__(self, key):
... return []
...
>>> a = ListDict()
>>> my_list = a[key]
>>> my_list
[]
>>> my_list.append("value")
>>> a[key] = my_list
>>> a
{'a': ['value']}
对于前三种方法,直接使用dict,setdefault会比前两种方法更高效。如果需要更灵活的对缺失值的处理,可以采用第四种方法(default_dict)和第五种方法(继承dict)。
缺失值处理的实质就是__missing__
这个特殊方法的调用,这个方法只会在__getitem__
(就是像d[k]这种形式)碰到缺失值时才会调用,而对get
,__contains__
(in方法)等其他dict的方法没有影响。所以在处理缺失值的时候,其他的方法也需要相应的处理。
其他类型的dict
collections.OrderedDict:按照key的插入顺序维护keys,可以以一个可预测的顺序对key进行遍历。还可以popitem第一个或者最一个元素。
collections.ChainMap:把几个dict放在一起进行搜索,搜索按照dict的顺序进行,在任意一个dict中找到key就会搜索成功。比如在变量查找时,可以先查找本地变量,再查找全局变量,最后查找系统变量。还有对命令行参数,可以先查找用户输入的变量,再查找变量的默认值。
collections.Counter:对每个key维护一个计数,可以通过update
方法更新一个key增加计数,还有most_common
返回出行次数最多的n个key。
Subclassing UserDict
一般创建一个新的mapping类型通过继承UserDict
会比继承dict
要容易。主要原因在于built-in对象的一些实现会走捷径,导致你必须去重载方法,而继承UserDict
不会有这些问题。具体的原因还会在后续的章节详细讲解。
Immutable Mappings
MappingProxyType
提供了一个原始mapping的动态识图,意味着原始的mapping的更新能够在这个mappingproxy中看到,但是不能通过他对原始的mapping进行更新。
>>> from types import MappingProxyType
>>> d={1:'A'}
>>> d_proxy = MappingProxyType(d)
>>> d_proxy
mappingproxy({1: 'A'})
>>> d_proxy[1]
'A'
>>> d_proxy[2] = 'x'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment
>>> d[2] = 'B'
>>> d_proxy
mappingproxy({1: 'A', 2: 'B'})
>>> d_proxy[2]
'B'
Set
set是一个唯一对象的集合,可以用于去重。set存储的元素必须是hashable的,但是set本身不是hashable的,但frozenset是hashable的。
>>> l = ['spam', 'spam', 'eggs', 'spam']
>>> set(l)
{'spam', 'eggs'}
>>> hash(set(l))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'set'
set提供一些中缀操作符,如a | b
表示union,a & b
表示intersection,a - b
表示difference。
创建set使用{},但是要注意一个空的集合需要使用set()
来创建,否则创建出来的是一个dict。
>>> s={1}
>>> type(s)
<class 'set'>
>>> s={}
>>> type(s)
<class 'dict'>
>>> s = {i for i in range(10)}
>>> s
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
>>> type(s)
<class 'set'>
dict and set Under the Hood
核心就是hash table,也是为什么dict和set的元素都需要是hashable的。不过和java相比,python解决hash冲突使用的是开放寻址法,而java使用的是链接法。使用开放寻址法导致内存开销较大,一旦无法解决冲突,就需要扩充内存,并且进行元素的重新分布。不过适合于key的快速查找。