字典类似Java中的Map,字典中的键可以是数字、字符串甚至是元组。
它们有一个共同特点就是不可变的,更进一步说,是可散列的数据类型。
可散列的: 在对象的生命周期汇总,它的散列值(hash code)是不变的,而且这个对象需要实现
__hash__()
和__eq__()
方法。如果两个可散列对象是相等的,那么它们的散列值一定是一样的。
上面说元组可以作为键是有个前提的,就是元组包含的所有元素都是可散列的情况下:
In [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函数,通过其他映射(字典)或者(键,值)对的序列建立字典
>>> 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'}
总结一下创建字典的方法:
In [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 "" , 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"
>>> d = {}
>>> d['name']='Jack'
>>> d
{'name': 'Jack'}
>>> d.clear()
>>> d
{}
>>> 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']}
使用给定的键建立新的字典,每个键对应的值默认为None,也可以指定:
>>> {}.fromkeys(('name','age'))
{'age': None, 'name': None}
>>> {}.fromkeys(('name','age'),'unknown')#指定'unknown'
{'age': 'unknown', 'name': 'unknown'}
>>> people
{'rose': 25, 'jack': 36}
>>> print people.get('a')
None
>>> people.get('rose')
25
还可以自定义默认值,当不存在时返回默认值:
>>> people.get('a',10)
10
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
弹出随机的项(键值对)
在不含有给定键的情况下设值,存在给定键则返回对应的值。
>>> d = {}
>>> d.setdefault('name','None')
'None'
>>> d
{'name': 'None'}
>>> d.setdefault('name','N/A')
'None'
>>> 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__
方法。
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
,接下来学习其他的映射类型。
LinkedHashMap
)。popitem([last=True|False])
返回字典里最后|第一个被添加进去的元素。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)]
我们来改进上面的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]:
{'§', '=', '¢', '#', '¤', '<', '¥', 'μ', '×', '$', '¶', '£', '©',
'°', '+', '÷', '±', '>', '¬', '®', '%'}
上图中列出了可变和不可变集合所拥有的方法的概况,其中不少方法是运算符重载的特殊方法。
在Python3中,key()
、items()
、values()
方法返回的都是字典视图,它们不可修改同时可以实时反馈字典的变化。
set
和frozenset
的实现依赖散列表,它们在散列表中只存放键而没有相应的值。(这点和Java的Set
实现类似)