Python容器:字典和集合

Published on 2017 - 01 - 20

字典

字典(dictionary)与列表类似,但其中元素的顺序无关紧要,因为它们不是通过像 0 或 1 的偏移量访问的。取而代之,每个元素拥有与之对应的互不相同的键(key),需要通过键来访问元素。键通常是字符串,但它还可以是 Python 中其他任意的不可变类型:布尔型、整型、浮点型、元组、字符串,以及其他一些在后面的内容中会见到的类型。字典是可变的,因此你可以增加、删除或修改其中的键值对。

如果使用过只支持数组或列表的语言,那么你很快就会爱上 Python 里的字典类型。

在其他语言中,字典可能会被称作关系型数组、哈希表或哈希图。在 Python 中,字典(dictionary)还经常会被简写成 dict。

使用{}创建字典

用大括号({})将一系列以逗号隔开的键值对(key:value)包裹起来即可进行字典的创建。最简单的字典是空字典,它不包含任何键值对:

>>> empty_dict = {}
>>> empty_dict
{}

使用dict()转换为字典

可以用 dict() 将包含双值子序列的序列转换成字典。(你可能会经常遇到这种子序列,例如“Strontium,90,Carbon,14”或者“Vikings,20,Packers,7”,等等。)每个子序列的第一个元素作为键,第二个元素作为值。

首先,这里有一个使用 lol(a list of two-item list)创建字典的小例子:

>>> lol = [ ['a', 'b'], ['c', 'd'], ['e', 'f'] ]
>>> dict(lol)
{'c': 'd', 'a': 'b', 'e': 'f'}

可以对任何包含双值子序列的序列使用 dict(),下面是其他例子。

包含双值元组的列表:

>>> lot = [ ('a', 'b'), ('c', 'd'), ('e', 'f') ]
>>> dict(lot)
{'c': 'd', 'a': 'b', 'e': 'f'}

包含双值列表的元组:

>>> tol = ( ['a', 'b'], ['c', 'd'], ['e', 'f'] )
>>> dict(tol)
{'c': 'd', 'a': 'b', 'e': 'f'}

双字符的字符串组成的列表:

>>> los = [ 'ab', 'cd', 'ef' ]
>>> dict(los)
{'c': 'd', 'a': 'b', 'e': 'f'}

双字符的字符串组成的元组:

>>> tos = ( 'ab', 'cd', 'ef' )
>>> dict(tos)
{'c': 'd', 'a': 'b', 'e': 'f'}

使用update()合并字典

使用 update() 可以将一个字典的键值对复制到另一个字典中去。

首先定义一个包含所有成员的字典 pythons:

>>> pythons = {
...     'Chapman': 'Graham',
...     'Cleese': 'John',
...     'Gilliam': 'Terry',
...     'Idle': 'Eric',
...     'Jones': 'Terry',
...     'Palin': 'Michael',
...     }
>>> pythons
{'Cleese': 'John', 'Gilliam': 'Terry', 'Palin': 'Michael',
'Chapman': 'Graham', 'Idle': 'Eric', 'Jones': 'Terry'}

接着定义一个包含其他喜剧演员的字典,命名为 others:

>>> others = { 'Marx': 'Groucho', 'Howard': 'Moe' }

现在,出现了另一个糟糕的程序员,它认为 others 应该被归入 Monty Python 成员中:

>>> pythons.update(others)
>>> pythons
{'Cleese': 'John', 'Howard': 'Moe', 'Gilliam': 'Terry',
'Palin': 'Michael', 'Marx': 'Groucho', 'Chapman': 'Graham',
'Idle': 'Eric', 'Jones': 'Terry'}

如果待添加的字典与待扩充的字典包含同样的键会怎样?是的,新归入字典的值会取代原有的值:

>>> first = {'a': 1, 'b': 2}
>>> second = {'b': 'platypus'}
>>> first.update(second)
>>> first
{'b': 'platypus', 'a': 1}

使用del删除具有指定键的元素

技术上来说,上面那个糟糕的程序员写的代码倒是正确的。但是他不应该这么做 !others 里的成员虽然也很搞笑很出名,但他们终归不是 Monty Python 的成员。把最后添加的两个成员清除出去:

>>> del pythons['Marx']
>>> pythons
{'Cleese': 'John', 'Howard': 'Moe', 'Gilliam': 'Terry',
'Palin': 'Michael', 'Chapman': 'Graham', 'Idle': 'Eric',
'Jones': 'Terry'}
>>> del pythons['Howard']
>>> pythons
{'Cleese': 'John', 'Gilliam': 'Terry', 'Palin': 'Michael',
'Chapman': 'Graham', 'Idle': 'Eric', 'Jones': 'Terry'}

使用clear()删除所有元素

使用 clear(),或者给字典变量重新赋值一个空字典({})可以将字典中所有元素删除:

>>> pythons.clear()
>>> pythons
{}
>>> pythons = {}
>>> pythons
{}

使用in判断是否存在

如果你希望判断某一个键是否存在于一个字典中,可以使用 in。我们来重新定义一下 pythons 字典,这一次可以省略一两个人名:

>>> pythons = {'Chapman': 'Graham', 'Cleese': 'John',
'Jones': 'Terry', 'Palin': 'Michael'}

测试一下看看谁在里面:

>>> 'Chapman' in pythons
True
>>> 'Palin' in pythons
True

这一次记得把 Terry Gilliam 添加到成员字典里了吗?

>>> 'Gilliam' in pythons
False

糟糕,好像又忘记了。

使用[key]获取元素

这是对字典最常进行的操作,只需指定字典名和键即可获得对应的值:

>>> pythons['Cleese']
'John'

如果字典中不包含指定的键,会产生一个异常:

>>> pythons['Marx']
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
KeyError: 'Marx'

有两种方法可以避免这种情况的发生。第一种是在访问前通过 in 测试键是否存在,就像在上一小节看到的一样:

>>> 'Marx' in pythons
False

另一种方法是使用字典函数 get()。你需要指定字典名,键以及一个可选值。如果键存在,会得到与之对应的值:

>>> pythons.get('Cleese')
'John'

反之,若键不存在,如果你指定了可选值,那么 get() 函数将返回这个可选值:

>>> pythons.get('Marx', 'Not a Python')
'Not a Python'

否则,会得到 None(在交互式解释器中什么也不会显示):

>>> pythons.get('Marx')
>>>

使用keys()获取所有键

使用 keys() 可以获得字典中的所有键。在接下来的几个例子中,我们将换一个示例:

>>> signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'}
>>> signals.keys()
dict_keys(['green', 'red', 'yellow'])

在 Python 2 里,keys() 会返回一个列表,而在 Python 3 中则会返回 dict_keys(),它是键的迭代形式。这种返回形式对于大型的字典非常有用,因为它不需要时间和空间来创建返回的列表。有时你需要的可能就是一个完整的列表,但在 Python 3 中,你只能自己调用 list() 将 dict_keys 转换为列表类型。

>>> list( signals.keys() )
['green', 'red', 'yellow']

在 Python 3 里,你同样需要手动使用 list() 将 values() 和 items() 的返回值转换为普通的 Python 列表。之后的例子中会用到这些。

使用values()获取所有值

使用 values() 可以获取字典中的所有值:

>>> list( signals.values() )
['go', 'smile for the camera', 'go faster']

使用items()获取所有键值对

使用 items() 函数可以获取字典中所有的键值对:

>>> list( signals.items() )
[('green', 'go'), ('red', 'smile for the camera'), ('yellow', 'go faster')]

每一个键值对以元组的形式返回,例如 ('green','go')。

使用=赋值,使用copy()复制

与列表一样,对字典内容进行的修改会反映到所有与之相关联的变量名上:

>>> signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'}
>>> save_signals = signals
>>> signals['blue'] = 'confuse everyone'
>>> save_signals
{'blue': 'confuse everyone', 'green': 'go',
'red': 'smile for the camera', 'yellow': 'go faster'}

若想避免这种情况,可以使用 copy() 将字典复制到一个新的字典中:

>>> signals = {'green': 'go', 'yellow': 'go faster', 'red': 'smile for the camera'}
>>> original_signals = signals.copy()
>>> signals['blue'] = 'confuse everyone'
>>> signals
{'blue': 'confuse everyone', 'green': 'go',
'red': 'smile for the camera', 'yellow': 'go faster'}
>>> original_signals
{'green': 'go', 'red': 'smile for the camera', 'yellow': 'go faster'}

集合

集合就像舍弃了值,仅剩下键的字典一样。键与键之间也不允许重复。如果你仅仅想知道某一个元素是否存在而不关心其他的,使用集合是个非常好的选择。如果需要为键附加其他信息的话,建议使用字典。

很久以前,当你还在小学时,可能就学到过一些关于集合论的知识。当然,如果你的学校恰好跳过了这一部分内容(或者实际上教了,但你当时正好盯着窗外发呆,我小时候就总是这样开小差),可以仔细看看图 1,它展示了我们对于集合进行的最基本的操作——交和并。

[图 1:集合的常见操作]

假如你将两个包含相同键的集合进行并操作,由于集合中的元素只能出现一次,因此得到的并集将包含两个集合所有的键,但每个键仅出现一次。空或空集指的是包含零个元素的集合。在图 1 中,名字以 X 开头的女性组成的集合就是一个空集。

使用set()创建集合

你可以使用 set() 函数创建一个集合,或者用大括号将一系列以逗号隔开的值包裹起来,如下所示:

>>> empty_set = set()
>>> empty_set
set()
>>> even_numbers = {0, 2, 4, 6, 8}
>>> even_numbers
{0, 8, 2, 4, 6}
>>> odd_numbers = {1, 3, 5, 7, 9}
>>> odd_numbers
{9, 3, 1, 5, 7}

与字典的键一样,集合是无序的。

由于 [] 能创建一个空列表,你可能期望 {} 也能创建空集。但事实上,{} 会创建一个空字典,这也是为什么交互式解释器把空集输出为 set() 而不是 {}。为何如此?没有什么特殊原因,仅仅是因为字典出现的比较早并抢先占据了花括号。

使用set()将其他类型转换为集合

你可以利用已有列表、字符串、元组或字典的内容来创建集合,其中重复的值会被丢弃。

首先来试着转换一个包含重复字母的字符串:

>>> set( 'letters' )
{'l', 'e', 't', 'r', 's'}

注意,上面得到的集合中仅含有一个 'e' 和一个 't',尽管字符串 'letters' 里各自包含两个。

再试试用列表建立集合:

>>> set( ['Dasher', 'Dancer', 'Prancer', 'Mason-Dixon'] )
{'Dancer', 'Dasher', 'Prancer', 'Mason-Dixon'}

再试试元组:

>>> set( ('Ummagumma', 'Echoes', 'Atom Heart Mother') )
{'Ummagumma', 'Atom Heart Mother', 'Echoes'}

当字典作为参数传入 set() 函数时,只有键会被使用:

>>> set( {'apple': 'red', 'orange': 'orange', 'cherry': 'red'} )
{'apple', 'cherry', 'orange'}

使用in测试值是否存在

这是集合里最常用的功能。我们来建立一个叫 drinks 的字典。每个键都是一种混合饮料的名字,与之对应的值是配料组成的集合:

>>> drinks = {
...     'martini': {'vodka', 'vermouth'},
...     'black russian': {'vodka', 'kahlua'},
...     'white russian': {'cream', 'kahlua', 'vodka'},
...     'manhattan': {'rye', 'vermouth', 'bitters'},
...     'screwdriver': {'orange juice', 'vodka'}
...     }

尽管都由花括号({ 和 })包裹,集合仅仅是一系列值组成的序列,而字典是一个或多个键值对组成的序列。

哪种饮料含有伏特加?(注意,后面例子中我会提前使用一点下一章出现的 for、if、and 以及 or 语句。)

>>> for name, contents in drinks.items():
...     if 'vodka' in contents:
...         print(name)
...
screwdriver
martini
black russian
white russian

我想挑的饮料需要有伏特加,但不含乳糖。此外,我很讨厌苦艾酒,觉得它尝起来就像煤油一样:

>>> for name, contents in drinks.items():
...     if 'vodka' in contents and not ('vermouth' in contents or
...         'cream' in contents):
...         print(name)
...
screwdriver
black russian

后面的内容会将上面的代码改写得更加简洁。

合并及运算符

如果想要查看多个集合之间组合的结果应该怎么办?例如,你想要找到一种饮料,它含有果汁或含有苦艾酒。我们可以使用交集运算符,记作 &:

>>> for name, contents in drinks.items():
...     if contents & {'vermouth', 'orange juice'}:
...         print(name)
...
screwdriver
martini
manhattan

& 运算符的结果是一个集合,它包含所有同时出现在你比较的两个清单中的元素。在上面代码中,如果 contents 里面不包含任何一种指定成分,则 & 会返回一个空集,相当于 False。

现在来改写一下上一小节的例子,就是那个我们想要伏特加但不需要乳脂也不需要苦艾酒的例子:

>>> for name, contents in drinks.items():
...     if 'vodka' in contents and not contents & {'vermouth', 'cream'}:
...         print(name)
...
screwdriver
black russian

将这两种饮料的原料都存储到变量中,以便于后面的例子不用再重复输入:

>>> bruss = drinks['black russian']
>>> wruss = drinks['white russian']

之后的例子会涵盖所有的集合运算符。有些运算使用特殊定义过的标点,另一些则使用函数,还有一些运算两者都可使用。这里使用测试集合 a(包含 1 和 2),以及 b(包含 2 和 3):

>>> a = {1, 2}
>>> b = {2, 3}

可以通过使用特殊标点符号 & 或者*集合函数 intersection() *获取集合的交集(两集合共有元素),如下所示:

>>> a & b
{2}
>>> a.intersection(b)
{2}

下面的代码片段使用了我们之前存储的饮料变量:

>>> bruss & wruss
{'kahlua', 'vodka'}

下面的例子中,使用 | 或者 union() 函数来获取集合的并集(至少出现在一个集合中的元素):

>>> a | b
{1, 2, 3}
>>> a.union(b)
{1, 2, 3}

还有酒精饮料的例子:

>>> bruss | wruss
{'cream', 'kahlua', 'vodka'}

使用字符 - 或者 difference() 可以获得两个集合的差集(出现在第一个集合但不出现在第二个集合):

>>> a - b
{1}
>>> a.difference(b)
{1}

>>> bruss - wruss
set()
>>> wruss - bruss
{'cream'}

到目前为止,出现的都是最常见的集合运算,包括交、并、差。出于完整性的考虑,我会在接下来的例子中列出其他的运算符,这些运算符并不常用。

使用 ^ 或者 symmetric_difference() 可以获得两个集合的异或集(仅在两个集合中出现一次):

>>> a ^ b
{1, 3}
>>> a.symmetric_difference(b)
{1, 3}

下面的代码帮我们找到了两种俄罗斯饮料的不同成分:

>>> bruss ^ wruss
{'cream'}

使用 <= 或者 issubset() 可以判断一个集合是否是另一个集合的子集(第一个集合的所有元素都出现在第二个集合中):

>>> a <= b
False
>>> a.issubset(b)
False

向“黑俄罗斯酒”中加入一些乳脂就变成了“白俄罗斯酒”,因此 wruss 是 bruss 的超集:

>>> bruss <= wruss
True

一个集合是它本身的子集吗?答案为:是的。

>>> a <= a
True
>>> a.issubset(a)
True

当第二个集合包含所有第一个集合的元素,且仍包含其他元素时,我们称第一个集合为第二个集合的真子集。使用 < 可以进行判断:

>>> a < b
False
>>> a < a
False

>>> bruss < wruss
True

超集与子集正好相反(第二个集合的所有元素都出现在第一个集合中),使用 >= 或者 issuperset() 可以进行判断:

>>> a >= b
False
>>> a.issuperset(b)
False

>>> wruss >= bruss
True

一个集合是它本身的超集:

>>> a >= a
True
>>> a.issuperset(a)
True

最后,使用 > 可以找到一个集合的真超集(第一个集合包含第二个集合的所有元素且还包含其他元素):

>>> a > b
False

>>> wruss > bruss
True

一个集合并不是它本身的真超集:

>>> a > a
False

参考文档