字典类似Java中的Map,字典中的键可以是数字、字符串甚至是元组。

它们有一个共同特点就是不可变的,更进一步说,是可散列的数据类型。

可散列的: 在对象的生命周期汇总,它的散列值(hash code)是不变的,而且这个对象需要实现​​__hash__()​​​和​​__eq__()​​方法。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。

上面说元组可以作为键是有个前提的,就是元组包含的所有元素都是可散列的情况下:

[33]: tl = (1,2,[30,40])                                                                                  

In [34]: hash(tl)
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-34-b7b49ff1c689> in <module>
----> 1 hash(tl)

TypeError: unhashable type: 'list'

In [36]: d = {tl:'1'}
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-36-8be9aeb1426b> in <module>
----> 1 d = {tl:'1'}

TypeError: unhashable type: 'list'

一般用户自定义的类型都是可散列的,散列值是它们的​​id()​​函数返回的值。

创建和使用字典

>>> people = {'jack':34,'rose':25,'jack':36}
>>> people
{'rose': 25, 'jack': 36}

>>> type(d)
dict

上面就是创建字典的方法,注意,字典中键是唯一的,我输入了两个’jack’,结果取的是后面输入的’jack’。

dict函数

也可以使用dict函数,通过其他映射(字典)或者(键,值)对的序列建立字典

>>> items = [('name','jack'),('age',24)]
>>> d = dict(items)
>>> d
{'age': 24, 'name': 'jack'}
>>> d['name']
'jack'

也可以通过关键字参数来创建字典:

>>> d = dict(name='rose',age=43)
>>> d
{'age': 43, 'name': 'rose'}

总结一下创建字典的方法:

[54]: a = dict(one=1,two=2,three=3)                                                                       

In [55]: b = {'one':1,'two':2,'three':3}

In [56]: c = dict(zip(['one','two','three'],[1,2,3]))

In [57]: d = dict([('two',2),('one',1),('three',3)])

In [58]: e = dict({'three':3,'one':1,'two':2})

In [59]: a == b == c == d == e
Out[59]: True

字典推导

字典推导可以从任何以键值对作为元素的可迭代对象中构建出字典。
下面展示了如何把一个装满元组的列表变成两个不同的字典:

In [66]: DIAL_CODES = [  
...: (86, 'China'),
...: (91, 'India'),
...: (1, 'United States'),
...: (62, 'Indonesia'),
...: (55, 'Brazil'),
...: (92, 'Pakistan'),
...: (880, 'Bangladesh'),
...: (234, 'Nigeria'),
...: (7, 'Russia'),
...: (81, 'Japan'),
...: ]

In [67]: country_code = {country : code for code, country in DIAL_CODES} #country 作为键,code作为值
In [69]: country_code
Out[69]:
{'China': 86,
'India': 91,
'United States': 1,
'Indonesia': 62,
'Brazil': 55,
'Pakistan': 92,
'Bangladesh': 880,
'Nigeria': 234,
'Russia': 7,
'Japan': 81}

In [70]: {code : country.upper() for country, code in country_code.items() if code < 66}
Out[70]: {1: 'UNITED STATES', 62: 'INDONESIA', 55: 'BRAZIL', 7: 'RUSSIA'}

字典的基本操作

字典中的键可以是任意的不可变类型,比如浮点型、字符串或元组。

字典的基本操作很多方面与序列类似:

  • ​len(d)​​ 返回d中项(键值对)的数量
  • ​d[k]​​返回k对应的值
>>> d
{'age': 43, 'name': 'rose'}
>>> d['age']
43
>>> d['a']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'a'

如果输入不存在的键,会报错。

  • ​d[k]=v​​将值v关联到键k上
>>> d['a'] = 1
>>> d
{'a': 1, 'age': 43, 'name': 'rose'}

可以关联一个不存在的键,那么就是添加一个新的键值对。如果关联已存在的键,则是覆盖。

  • ​del d[k]​​删除键为k的项
  • ​k in d​​检查d是否含有键为k的项

字典的格式化字符串

可以在%后面加上键:

>>> people
{'rose': 25, 'jack': 36}
>>> " rose's age is %(rose)s" % people #%(rose)s 在%s中间加入了(rose)
" rose's age is 25"

字典方法

  • clear
    用来清除字典中所有的项。这个是原地操作,也就是无返回值。
>>> d = {}
>>> d['name']='Jack'
>>> d
{'name': 'Jack'}
>>> d.clear()
>>> d
{}
  • copy
    返回一个具有相同键值对的新字典,该方法实现的是浅克隆。
>>> x = {'username': 'admin','machines': ['linux1','centos1','windows10']}
>>> y = x.copy()
>>> y['username'] = 'mlh'
>>> y['machines'].remove('linux1')
>>> y
{'username': 'mlh', 'machines': ['centos1', 'windows10']}
>>> x
{'username': 'admin', 'machines': ['centos1', 'windows10']}

可以看到,在副本中替换值的时候,原始字典不受影响,但是如果修改了某个值(原地修改),原始字典也会改变。

其实因为浅克隆拷贝的只是引用,替换值就是替换成了其他引用,是不会影响原始字典的。而修改值,修改的是同一个引用指向的内存空间,因此都会影响。

避免这个问题的一种方法就是深克隆:

>>> from copy import deepcopy
>>> d = {}
>>> d['names'] = ['Alfred','Bertrand']
>>> c = d.copy()
>>> dc = deepcopy(d)
>>> d['names'].append('Clive')
>>> c
{'names': ['Alfred', 'Bertrand', 'Clive']}
>>> dc
{'names': ['Alfred', 'Bertrand']}
  • fromkeys

使用给定的键建立新的字典,每个键对应的值默认为None,也可以指定:

>>> {}.fromkeys(('name','age'))
{'age': None, 'name': None}
>>> {}.fromkeys(('name','age'),'unknown')#指定'unknown'
{'age': 'unknown', 'name': 'unknown'}
  • get
    get访问字典中不存在的项时不会出错,并返回None。
>>> people
{'rose': 25, 'jack': 36}
>>> print people.get('a')
None
>>> people.get('rose')
25

还可以自定义默认值,当不存在时返回默认值:

>>> people.get('a',10)
10
  • items和iteritems

​items​​方法将字典的所有项以列表的方式返回:

>>> people.items()
[('rose', 25), ('jack', 36)]

​iteritems​​方法返回一个迭代器对象:

>>> it = people.iteritems()
>>> it
<dictionary-itemiterator object at 0x1baa7e0>
>>> list(it) #将迭代器转换为list
[('rose', 25), ('jack', 36)]
  • pop
    获取给定键的值,并将这项从字典中移除
  • popitem

弹出随机的项(键值对)

  • setdeafult

在不含有给定键的情况下设值,存在给定键则返回对应的值。

>>> d = {}
>>> d.setdefault('name','None')
'None'
>>> d
{'name': 'None'}
>>> d.setdefault('name','N/A')
'None'
  • update
    可以用一个字典项更新另外一个字典:
>>> people
{'rose': 25, 'jack': 36}
>>> x = {'jack':24}
>>> people.update(x)
>>> people
{'rose': 25, 'jack': 24}

x中如果有people中不存在的项,也会更新进去:

>>> people
{'rose': 25, 'jack': 24}
>>> x = {'jack':21,'cherry':20}
>>> people.update(x)
>>> people
{'rose': 25, 'jack': 21, 'cherry': 20}

映射的弹性键查询

有时候为了方便,就算某个键在映射(这里指字典)里不存在,我们也希望能获取一个默认值。此时有两个途径可以实现这个目的,一是通过​​defaultdict​​​这个类型而不是普通的​​dict​​​,另一个是自定义​​dict​​​的子类,在子类中实现​​__missing__​​方法。

defaultdict

import sys
import re
import collections

WORD_RE = re.compile(r'\w+')

index = collections.defaultdict(list) #以list构造方法作为default_factory来创建一个defaultdict
with open(sys.argv[1],encoding='utf-8') as fp:
for line_no,line in enumerate(fp,1):
for match in WORD_RE.finditer(line):#返回索引从1开始的序列,值为文件中的行
word = match.group()
column_no = match.start() + 1
location = (line_no,column_no)
index[word] .append(location) #如果index中没有word的记录,那么会返回一个空列表

for word in sorted(index,key=str.upper):
print(word,index[word])

以当前文件作为参数传入,结果输出如下(部分结果):

append [(16, 26)]
argv [(10, 15)]
as [(10, 41)]
coding [(1, 7)]
collections [(5, 8), (9, 9)]
column_no [(14, 13), (15, 33)]
compile [(7, 14)]
defaultdict [(9, 21)]
encoding [(10, 23)]
enumerate [(11, 25)]
finditer [(12, 30)]
for [(11, 5), (12, 9), (18, 1)]
fp [(10, 44), (11, 35)]
group [(13, 26)]
import [(3, 1), (4, 1), (5, 1)]
in [(11, 22), (12, 19), (18, 10)]
index [(9, 1), (16, 13), (18, 20), (19, 16)]
...

所有这一切背后的功臣其实是魔法方法​​__missing__​​​,它会在​​defaultdict​​​遇到找不到的键时调用​​default_factory​​。

__missing__

虽然​​dict​​​没有定义这个方法,但是它是知道这个方法的存在的。如果某个类继承了​​dict​​​,然后提供了​​__missing__​​​方法,那么在​​__getitem__​​找不到键的时候,就会自动调用它。

我们定义一个在查询的时候,将映射类型里面的键转换str的类:

# -*- coding: utf-8 -*
class StrKeyDict0(dict):

def __missing__(self, key):
if isinstance(key,str): #如果找不到的key本身就是字符串,则抛错
raise KeyError(key)
return self[str(key)] #否则转换成字符串再查找

def get(self, key, default=None):
try:
return self[key] #把查找用self[key]的形式委托给__getitem__
except KeyError:
return default

def __contains__(self, key):
return key in self.keys() or str(key) in self.keys()

if __name__ == '__main__':
d = StrKeyDict0([('2','two'),('4','four')])
print(d['2'])#two
#print(d[1]) #KeyError: '1'
print(d.get('2'))#two
print(d.get(1,'N/A'))#N/A

字典的变种

上面我们学习了​​dict​​​和​​defaultdict​​,接下来学习其他的映射类型。

  • collections.OrderedDict 在添加键的时候会保持顺序,因此键的迭代顺序是一致的(类似Java中的​​LinkedHashMap​​​)。​​popitem([last=True|False])​​返回字典里最后|第一个被添加进去的元素。
  • collections.ChainMap 可以容纳数个不同的映射对象,然后在进行键查找操作的时候,这些对象会被当作一个整体被逐个查找,直到键被找到为止。相当于是一个字典的集合。
  • collections.Counter 给键准备一个整数计数器。每次更新一个键的时候都会增加这个计数器。所以这个类型可以用来给可散列表对象计数。
In [2]: import collections                                                                                   

In [3]: ct = collections.Counter('abcdabcdabcard')

In [4]: ct
Out[4]: Counter({'a': 4, 'b': 3, 'c': 3, 'd': 3, 'r': 1})

In [5]: ct.update('aaaaaazzz')

In [6]: ct
Out[6]: Counter({'a': 10, 'b': 3, 'c': 3, 'd': 3, 'r': 1, 'z': 3})

In [7]: ct.most_common(2) # 返回出现次数最多的两个键和计数
Out[7]: [('a', 10), ('b', 3)]
  • colllections.UserDict 这个类其实就是把标准 dict 用纯 Python 又实现了一遍,主要是提供给用户来扩展的。

子类化UserDict

我们来改进上面的​​StrKeyDict0​​类,使得所有的键都存储为字符串类型。

要注意的是​​UserDict​​​并不是​​dict​​​的子类,但它有一个名为​​data​​​的​​dict​​​实例属性(这里有点体现了组合优于继承)。这样做的好处是,​​UserDict​​​的子类就能在实现​​__setitem__​​​时避免不必要的递归,也可以让​​__contains__​​里的代码更简洁。

# -*- coding: utf-8 -*
import collections

class StrKeyDict(collections.UserDict):

def __missing__(self, key):
if isinstance(key,str): #如果找不到的key本身就是字符串,则抛错
raise KeyError(key)
return self[str(key)] #否则转换成字符串再查找

def __contains__(self, key):
return str(key) in self.data #

def __setitem__(self, key, value):
self.data[str(key)] = value #该方法会把所有的键都转换成字符串

if __name__ == '__main__':
d = StrKeyDict([('2','two'),('4','four')])
print(d['2'])#two
#print(d[1]) #KeyError: '1'
print(d.get('2'))#two
print(d.get(1,'N/A'))#N/A

不可变字典类型

有时候我们需要不可变的字典类型,比如在返回某个字典给用户,但又不想让用户修改该字典。从Python3.3开始,​​types​​​模块中引入了一个封装类名叫​​MappingProxyType​​,如果给该类一个映射,它会返回一个只读的映射视图。虽然是只读的,但是能观察到原映射的修改。

In [1]: from types import MappingProxyType                                                                   

In [2]: d = {1:'A'}

In [3]: d_proxy = MappingProxyType(d)

In [4]: d_proxy
Out[4]: mappingproxy({1: 'A'})

In [5]: d_proxy[2] = 'x'
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-5-bc17a9a62754> in <module>
----> 1 d_proxy[2] = 'x'

TypeError: 'mappingproxy' object does not support item assignment

In [6]: d[2] = 'B'

In [7]: d_proxy
Out[7]: mappingproxy({1: 'A', 2: 'B'})

集合

集合是由可迭代对象构造的,可用于去重。
集合是可变的,但本身只能包含不可变(可散列)的值。

>>> set([0,1,2,3,0,1,2,3,4,5])
set([0, 1, 2, 3, 4, 5])

还可以通过字面量来构造:

In [21]: s = {1}                                                                                             
In [22]: type(s)
Out[22]: set
In [23]: s
Out[23]: {1}
In [24]: s.pop()
Out[24]: 1
In [25]: s
Out[25]: set()

像​​{1,2,3}​​​这种字面量相比​​set([1,2,3])​​要更快且更易读。

可以求并集和交集:

>>> a = set([1,2,3])
>>> b = set([2,3,4])
>>> a.union(b)
set([1, 2, 3, 4])
>>> a | b #交集
set([1, 2, 3, 4])
>>> c = a & b #并集
>>> c
set([2, 3])
>>> c.issubset(a)
True
>>> c <= a
True
>>> c.issuperset(a)
False
>>> c >= a
False
>>> a.intersection(b)
set([2, 3])
>>> a & b
set([2, 3])
>>> a.difference(b) # 差集
set([1])
>>> a - b
set([1])

可以看到,python重载了很多运算符可以很方便的操作set集合。

集合是可变的,也是不可变的集合类型​​frozenset​​。

In [34]: frozenset(range(10))                                                                                
Out[34]: frozenset({0, 1, 2, 3, 4, 5, 6, 7, 8, 9})

集合推导

Python2.7带来了集合推导和字典推导。

In [37]: from unicodedata import name                                                                        
In [42]: {chr(i) for i in range(32, 256) if 'SIGN' in name(chr(i),'')} # 新建一个 Latin-1 字符集合,该集合里的每个字符的Unicode 名字里都有“SIGN”这个单词
Out[42]:
{'§', '=', '¢', '#', '¤', '<', '¥', 'μ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}

集合的操作

Python中的字典与集合_Python字典操作

上图中列出了可变和不可变集合所拥有的方法的概况,其中不少方法是运算符重载的特殊方法。

在Python3中,​​key()​​​、​​items()​​​、​​values()​​方法返回的都是字典视图,它们不可修改同时可以实时反馈字典的变化。

​set​​​和​​frozenset​​​的实现依赖散列表,它们在散列表中只存放键而没有相应的值。(这点和Java的​​Set​​实现类似)