讨论Python 语言中的映射类型和集合类型
首先做一个介绍;
可用操作符,工厂函数、内建函数(BIF)和方法;
每种数据类型的详细用法;
字典是Python 语言中唯一的映射类型。映射类型对象里哈希值(键) 和指向的对象(值)是一对多的关系,通常被认为是可变的哈希表。
一个字典对象是可变的,它是一个容器类型,能存储任意个数的Python 对象,其中也包括其他容器类型。字典类型和序列类型容器类(列表、元组)的区别是存储和访问数据的方式不同。
序列类型用数字类型的键(从序列的开始起按数值顺序索引)。
映射类型用其他对象类型做键;一般最常见的是用字符串做键(keys)。
和序列类型的键不同,映射类型的键(keys)直接,或间接地和存储的数据值相关联。但因为在映射类型中,我们不再用"序列化排序"的键(keys),所以映射类型中的数据是无序排列的。
你可以用键(key)直接 "映射" 到值, 这就是为什么叫映射类型的原因。映射类型通常被称做哈希表的原因是字典对象就是哈希类型的。
核心笔记:什么是哈希表?它们与字典的关系是什么?
序列类型用有序的数字键做索引将数据以数组的形式存储。一般,索引值与所存储的数据毫无关系。
还可以用另一种方式来存储数据:基于某种相关值,比如说一个字符串。你把人们的电话号码按照他们的姓记录在电话簿上,等等。在这些例子中,你的键(key)就是和数据项相关的值。
哈希表是一种数据结构:哈希表中存储的每一值(value),是根据与它相关的一个被称作为键(key)的数据项进行存储的。键和值合在一起被称为“键-值 对“。 哈希表的算法是获取键,对键执行一个叫做哈希函数的操作,并根据计算的结果,选择在数据结构的某个地址中来存储你的值。任何一个值存储的地址皆取决于它的键。正因为这种随意性,哈希表中的值是没有顺序的。你拥有的是一个无序的数据集。
你所能获得的有序集合只能是字典中的键的集合或者值的集合。方法Keys() 或 values() 返回一个列表,该列表是可排序的。 你还可以用 items()方法得到包含键、值对的元组的列表来排序。
由于字典本身是哈希的,所以是无序的。
哈希表一般有很好的性能, 因为用键查询相当快。
Python 的字典是作为可变的哈希表实现的。如果你熟悉Perl 的话, 就可以发现字典与Perl 中的"关系数组"或散列相似。
现在我们就来研究Python 字典。一个字典条目的语法格式是 键:值。 而且,多条字典条目被包含在( { } ) 里。
如何创建字典和给字典赋值
创建字典只需要把字典赋值给一个变量,不管这个字典是否包含元素:
>>> dict1 = {}
>>> dict2 = {'name': 'earth', 'port': 80}
>>> dict1, dict2
({}, {'port': 80, 'name': 'earth'})
可以用工厂方法 dict() 来创建字典。 当我们详细讨论dict()的时候会看到更多的例子:
>>> fdict = dict((['x', 1], ['y', 2]))
>>> fdict
{'y': 2, 'x': 1}
可以用一个很方便的内建方法fromkeys() 来创建一个"默认"字典, 字典中元素具有相同的值 (如果没有给出, 默认为None):
>>> ddict = {}.fromkeys(('x', 'y'), -1)
>>> ddict
{'y': -1, 'x': -1}
>>> edict = {}.fromkeys(('foo', 'bar'))
>>> edict
{'foo': None, 'bar': None}
如何访问字典中的值
要想遍历一个字典(一般用键), 你只需要循环查看它的键, 像这样:
>>> dict2 = {'name': 'earth', 'port': 80}
>>>> for key in dict2.keys():
... print 'key=%s, value=%s' % (key, dict2[key])
...
key=name, value=earth
key=port, value=80
可以不必再用keys()方法获取供循环使用的键值列表了。 可以用迭代器来轻松地访问类序列对象(sequence-like objects),比如字典和文件。只需要用字典的名字就可以在 for 循环里遍历字典。
>>> dict2 = {'name': 'earth', 'port': 80}
>>>
>>>> for key in dict2:
... print 'key=%s, value=%s' % (key, dict2[key])
...
key=name, value=earth
key=port, value=80
要得到字典中某个元素的值, 可以用你所熟悉的字典键加上中括号来得到:
>>> dict2['name']
'earth'
>>> print 'host %s is running on port %d' % \
... (dict2['name'], dict2['port'])
host earth is running on port 80
如果我们想访问该字典中的一个数据元素,而它在这个字典中没有对应的键,将会产生一个错误:
>>> dict2['server'] Traceback (innermost last):
File "<stdin>", line 1, in ?
KeyError: server
检查一个字典中是否有某个键的最好方法是用字典的 has_key()方法, 或者另一种比较好的方法 in 或 not in 操作符。 has_key() 方法将会在未来的Python 版本中弃用,所以用in 或 not in 是最好的方法。
下面我们将介绍字典所有的方法。方法has_key()和 in 以及 not in 操作符都是布尔类型的。
对于前两者而言,如果字典中有该键就返回真(True),否则返回假(False):
>>> 'server' in dict2 # 或 dict2.has_key('server')
False
>>> 'name' in dict # 或 dict2.has_key('name')
True
>>> dict2['name']
'earth'
一个字典中混用数字和字符串的例子:
>>> dict3 = {}
>>> dict3[1] = 'abc'
>>> dict3['1'] = 3.14159
>>> dict3[3.2] = 'xyz'
>>> dict3
{3.2: 'xyz', 1: 'abc', '1': 3.14159}
除了逐一地添加每个键-值对外,我们也可以给dict3 整体赋值。
dict3 = {3.2: 'xyz', 1: 'abc', '1': 3.14159}
所有的数据就可以用键-值对来创建一个字典。通过字典dict3 的示例说明你可以采用各种类型的数据作为字典的键。
为什么在执行中字典中的键不允许被改变呢? 比方说, 你创建了一个字典,字典中包含一个元素(一个键和一个值)。可能是由于某个变量的改变导致键发生了改变。这时候你如果用原来的键来取出字典里的数据,会得到KeyError(因为键的值已经改变了),现在你没办法从字典中获取该值了,因为键本身的值发生了变化。由于上面的原因,字典中的键必须是可哈希的, 所以数字和字符串可以作为字典中的键, 但是列表和其他字典不行。(见7.5.2 小节 字典的键必须是可哈希的)
如何更新字典
你可以通过以下几种方式对一个字典做修改:添加一个新数据项或新元素(即,一个键-值对);
修改一个已存在的数据项;或删除一个已存在的数据项(下面有关于数据项删除操作的详细讲述).
>>> dict2['name'] = 'venus' # 更新已有条目
>>> dict2['port'] = 6969 # 更新已有条目
>>> dict2['arch'] = 'sunos5'# 增加新条目
>>> print 'host %(name)s is running on port %(port)d' %dict2
host venus is running on port 6969
如果字典中该键已经存在,则字典中该键对应的值将被新值替代。上面的print 语句展示了另一种在字典中使用字符串格式符( %)的方法。用字典参数可以简化print 语句,因为这样做你只须用到一次该字典的名字,而不用在每个元素出现的时候都用元组参数表示。
可以用内建方法update()将整个字典的内容添加到另一个字典。我们将在7.4 节介绍此方法。
如何删除字典元素和字典
删除整个字典的操作不常见。通常,你删除字典中的单个元素或是清除整个字典的内容。但是,如果你真想"删除"一个字典,用del 语句:
del dict2['name'] # 删除键为“name”的条目
dict2.clear() # 删除dict2 中所有的条目
del dict2 # 删除整个dict2 字典
dict2.pop('name') # 删除并返回键为“name”的条目
核心笔记:避免使用内建对象名字作为变量的标识符
如果在Python 2.3 前,你可能用dict 作为一个字典的标识符。但是,因为 dict() 现在已成为 Python 的类型和工厂方法,重载dict()会给你带来麻烦和潜在的bugs。
编译器允许你做这样的重载,它认为你是聪明的,知道自己正在做什么!小心。请不要用 dict, list,file, bool, str, input, len 这样的内建类型为变量命名。
字典可以和所有的标准类型操作符一起工作,但却不支持像拼接(concatenation)和重复(repetition)这样的操作。这些操作对序列有意义,可对映射类型行不通。在接下来的两小节里,我们将向你讲述字典中的操作符。
标准类型操作符已在第四章介绍。 下面是一些使用操作符的简单示例:
>>> dict4 = {'abc': 123}
>>> dict5 = {'abc': 456}
>>> dict6 = {'abc': 123, 98.6: 37}
>>> dict7 = {'xyz': 123}
>>> dict4 < dict5
True
>>> (dict4 < dict6) and (dict4 < dict7)
True
>>> (dict5 < dict6) and (dict5 < dict7)
True
>>> dict6 < dict7
False
字典是如何比较的呢? 与列表和元组一样,这个过程比数字和字符串的比较更复杂些。详细算法请见第7.3.1 小节。
字典的键查找操作符([ ])
键查找操作符是唯一仅用于字典类型的操作符,它和序列类型里单一元素的切片(slice)操作符很相象。
对序列类型来说,用索引做唯一参数或下标(subscript)以获取一个序列中某个元素的值。
对字典类型来说,是用键(key)查询(字典中的元素),所以键是参数(argument), 而不是一个索引(index)。
键查找操作符既可以用于给字典赋值,也可以用于从字典中取值:
d[k] = v 通过键'k',给字典中某元素赋值'v'
d[k] 通过键'k',查询字典中某元素的值
(键)成员关系操作( in ,not in)
可以不用has_key()方法,而用 in 和 not in 操作符来检查某个键是否存在于字典中:
>>> 'name' in dict2
True
>>> 'phone' in dict2
False
对一个字典调用type()工厂方法,会返回字典类型, “<type 'dict'>”. 调用str()工厂方法将返回该字典的字符串表示形式,这些都很容易理解。
cmp() 内建函数对字典是如何比较的呢? 字典是通过这样的算法来比较的: 首先是字典的大小,然后是键,最后是值。可是,用 cmp() 做字典的比较一般不是很有用。
接下来的小节里,将进一步详细说明字典比较的算法,但这部分是高层次的阅读内容,可以跳过,因为字典的比较不是很有用也不常见。
*字典比较算法
接下来的例子中,我们建立两个字典进行比较,然后慢慢修改,来看看这些修改对它们之间的比较带来的影响:
>>> dict1 = {}
>>> dict2 = {'host': 'earth', 'port': 80}
>>> cmp(dict1, dict2) # dict1 比 dict2 小,因为dict2 有更多元素(2 个 vs. 0 个)
-1
>>> dict1['host'] = 'earth'
>>> cmp(dict1, dict2) # 在向dict1 加一个元素后,dict1 仍然比dict2 小 (2 vs. 1)
-1
===============
>>> dict1['port'] = 8080
>>> cmp(dict1, dict2)
1
>>> dict1['port'] = 80
>>> cmp(dict1, dict2)
0
在向dict1 中添加第二个元素后,两个字典的长度相同,所以用键比较大小。这时键相等,则通过它们的值比较大小。键 'host'的值相同,对于键 'port',dict1 中值比dict2 中的值大(8080 vs.80)。当把dict2 中'port'的值设成和dict1 中的值一样,那么两个字典相等:它们有相同的大小、相同的键、相同的值,所以cmp() 返回值是0。
>>> dict1['prot'] = 'tcp'
>>> cmp(dict1, dict2)
1
>>> dict2['prot'] = 'udp'
>>> cmp(dict1, dict2)
-1
当向两个字典中的任何一个添加新元素时,这个字典马上会成为大的那个字典,就像例子中的dict1 一样。向dict2 添加键-值对后,因为两个字典的长度又相等了,会继续比较它们的键和值。
>>> cdict = {'fruits':1}
>>> ddict = {'fruits':1}
>>> cmp(cdict, ddict)
0
>>> cdict['oranges'] = 0
>>> ddict['apples'] = 0
>>> cmp(cdict, ddict)
14
上面的例子表明cmp()可以返回除-1,0,1 外的其他值。算法按照以下的顺序。
(1)比较字典长度
如果字典的长度不同,那么用 cmp(dict1, dict2) 比较大小时,如果字典 dict1 比 dict2 长,cmp()返回正值,如果 dict2 比 dict1 长,则返回负值。也就是说,字典中的键的个数越多,这个字典就越大,即:
len(dict1) > len(dict2) ==> dict1 > dict2
(2)比较字典的键
如果两个字典的长度相同,那就按字典的键比较;键比较的顺序和 keys()方法返回键的顺序相同。 (注意: 相同的键会映射到哈希表的同一位置,这保证了对字典键的检查的一致性。) 这时,如果两个字典的键不匹配时,对这两个(不匹配的键)直接进行比较。当dict1 中第一个不同的键大于dict2 中第一个不同的键,cmp()会返回正值。
(3)比较字典的值
如果两个字典的长度相同而且它们的键也完全匹配,则用字典中每个相同的键所对应的值进行比较。一旦出现不匹配的值,就对这两个值进行直接比较。若dict1 比dict2 中相同的键所对应的值大,cmp()会返回正值。
(4) Exact Match
到此为止,即,每个字典有相同的长度、相同的键、每个键也对应相同的值,则字典完全匹配,返回0 值。
dict()
工厂函数被用来创建字典。如果不提供参数,会生成空字典。当容器类型对象做为一个参数传递给方法dict() 时很有意思。如果参数是可以迭代的,即,一个序列,或是一个迭代器,或是一个支持迭代的对象,那每个可迭代的元素必须成对出现。在每个值对中,第一个元素是字典的键、第二个元素是字典中的值。见Python 文档里关于dict()的例子:
>>> dict(zip(('x', 'y'), (1, 2)))
{'y': 2, 'x': 1}
>>> dict([['x', 1], ['y', 2]])
{'y': 2, 'x': 1}
>>> dict([('xy'[i-1], i) for i in range(1,3)])
{'y': 2, 'x': 1}
如果输入参数是(另)一个映射对象,比如,一个字典对象,对其调用dict()会从存在的字典里复制内容来生成新的字典。新生成的字典是原来字典对象的浅复制版本, 它与用字典的内建方法copy() 生成的字典对象是一样的。但是从已存在的字典生成新的字典速度比用copy()方法慢,我们推荐使用copy()。
调用dict()方法可以接受字典或关键字参数字典(函数操作符,第11 章):
>>> dict(x=1, y=2)
{'y': 2, 'x': 1}
>>> dict8 = dict(x=1, y=2)
>>> dict8
{'y': 2, 'x': 1}
>>> dict9 = dict(**dict8)
>>> dict9
{'y': 2, 'x': 1}
我们提醒读者dict9 的例子只作为了解dict()方法的用途,它不是现实中的例子。使用下面这些行的方法更聪明(效率更好):
>>> dict9 = dict8.copy()
>>> dict9
{'y': 2, 'x': 1}
len()
内建函数len()很灵活。它可用在序列、映射类型和集合上(在本章的后面我们会看到)。对字典调用len(),它会返回所有元素(键-值对)的数目:
>>> dict2 = {'name': 'earth', 'port': 80}
>>> dict2
{'port': 80, 'name': 'earth'}
>>> len(dict2)
2
我们前面提到字典中的元素是没有顺序的。从上面的例子中可以看到,dict2 的元素显示的顺序和输入时的顺序正相反。
hash()
内建函数hash()本身并不是为字典设计的方法,但它可以判断某个对象是否可以做一个字典的键。将一个对象作为参数传递给 hash(), 会返回这个对象的哈希值。 只有这个对象是可哈希的,才可作为字典的键 (函数的返回值是整数,不产生错误或异常)。
如果用比较操作符来比较两个数值,发现它们是相等的,那么即使二者的数据类型不同, 它们也会得到相同的哈希值。
如果非可哈希类型作为参数传递给hash()方法,会产生TypeError 错误(因此,如果使用这样的对象作为键给字典赋值时会出错):
>>> hash([])
Traceback (innermost last): File "<stdin>", line 1, in ?
TypeError: list objects are unhashable
>>>
>>> dict2[{}] = 'foo'
Traceback (most recent call last): File "<stdin>", line 1, in ?
TypeError: dict objects are unhashable
表7.1 映射类型的相关函数
字典提供了大量方法来帮你做事情,见表 7.2.
我们说明字典的一些很常见的方法。我们已经看到 has_key() 和它的替代方法 in 和 not in。
keys()方法,返回一个列表,包含字典中所有的键;
values()方法,返回一个列表;
包含字典中所有的值,items(), 返回一个包含所有(键, 值)元组的列表;
这些方法在不按任何顺序遍历字典的键或值时很有用。
>>> dict2.keys()
['port', 'name']
>>> dict2.values()
[80, 'earth']
>>> dict2.items()
[('port', 80), ('name', 'earth')]
>>> for eachKey in dict2.keys():
... print 'dict2 key', eachKey, 'has value', dict2[eachKey]
...
dict2 key port has value 80
dict2 key name has value earth
keys()方法很有用,它返回一个包含字典中所有键的列表,此方法可以与for 循环一起使用来获取字典中的值。
表 7.2 字典类型方法
但是,它返回的元素是没有顺序的(和哈希表中的键(keys)一样),我们通常希望它们能按某种方式排序。
以前,只能调用字典的keys()方法获得键的列表,然后调用列表的sort()方法得到一个有序可遍历的列表。现在特别为迭代子设计了一个名为 sorted()的内建函数,它返回一个有序的迭代子:
>>> for eachKey in sorted(dict2):
... print 'dict2 key', eachKey, 'has value',dict2[eachKey]
...
dict2 key name has value earth
dict2 key port has value 80
update()方法可以用来将一个字典的内容添加到另外一个字典中。字典中原有的键如果与新添加的键重复,那么重复键所对应的原有条目的值将被新键所对应的值所覆盖。原来不存在的条目则被添加到字典中。clear()方法可以用来删除字典中的所有的条目。
>>> dict2= {'host':'earth', 'port':80}
>>> dict3= {'host':'venus', 'server':'http'}
>>> dict2.update(dict3)
>>> dict2
{'server': 'http', 'port': 80, 'host': 'venus'}
>>> dict3.clear()
>>> dict3
{}
copy() 方法返回一个字典的副本。注意这只是浅复制。
最后要说明,get()方法和键查找(key-lookup)操作符( [ ] )相似,不同的是它允许你为不存在的键提供默认值。如果该键不存在,也未给出它的默认值,则返回None。此方法比采用键查找(key-lookup)更灵活,因为你不必担心因键不存在而引发异常。
>>> dict4 = dict2.copy()
>>> dict4
{'server': 'http', 'port': 80, 'host': 'venus'}
>>> dict4.get('host')
'venus'
>>> dict4.get('xxx')
>>> type(dict4.get('xxx'))
<type 'None'>
>>> dict4.get('xxx', 'no such key')
'no such key'
setdefault()是实现了常用的语法: 检查字典中是否含有某键。 如果字典中这个键存在,你可以取到它的值。 如果所找的键在字典中不存在,你可以给这个键赋默认值并返回此值:
>>> myDict = {'host': 'earth', 'port': 80}
>>> myDict.keys()
['host', 'port']
>>> myDict.items()
[('host', 'earth'), ('port', 80)]
>>> myDict.setdefault('port', 8080)
80
>>> myDict.setdefault('prot', 'tcp')
'tcp'
>>> myDict.items()
[('prot', 'tcp'), ('host', 'earth'), ('port', 80)]
前面,我们曾简要介绍过fromkeys()方法,下面是更多的示例:
>>> {}.fromkeys('xyz')
{'y': None, 'x': None, 'z': None}
>>>
>>> {}.fromkeys(('love', 'honor'), True)
{'love': True, 'honor': True}
目前,keys(), items(), 和 values()方法的返回值都是列表。数据集如果很大会导致很难处理,这也正是iteritems(), iterkeys(), 和itervalues() 方法被添加到 Python 2.2 的主要原因。这些函数与返回列表的对应方法相似,只是它们返回惰性赋值的迭代器,所以节省内存。未来的Python 版本中,甚至会更灵活,那时这些方法将会返回强大的对象,暂叫做视图(views)。视图(views)是访问容器对象的接口集。举例来说,你可以从一个视图(views)中删除某个字典的键,从而改变某个字典。
字典中的值没有任何限制。 他们可以是任意Python 对象,即,从标准对象到用户自定义对象皆可。但是字典中的键是有类型限制的。
每个键只能对应一个项。也就是说,一键对应多个值是不允许的。(像列表、元组和其他字典这样的容器对象是可以的。) 当有键发生冲突(即,字典键重复赋值),取最后(最近)的赋值。
>>> dict1 = {' foo':789, 'foo': 'xyz'}
>>> dict1
{'foo': 'xyz'}
>>> dict1['foo'] = 123
>>> dict1
{'foo': 123}
Python 并不会因字典中的键存在冲突而产生一个错误,它不会检查键的冲突是因为,若在每个键-值对赋值的时候都会做检查,这将会占用一定量的内存。
当给字典中一个不存在的键赋值时,键和值会被创建和添加,但如果该键已经存在(键冲突),那此键所对应的值将被替换。
大多数Python 对象可以作为键;但它们必须是可哈希的对象。像列表和字典这样的可变类型,由于它们不是可哈希的,所以不能作为键。
所有不可变的类型都是可哈希的,因此它们都可以做为字典的键。一个要说明的是问题是数字:值相等的数字表示相同的键。换句话来说,整型数字 1 和 浮点数 1.0 的哈希值是相同的,即它们是相同的键。
同时,也有一些可变对象(很少)是可哈希的,它们可以做字典的键,但很少见。举一个例子,一个实现了__hash__() 特殊方法的类。因为__hash__()方法返回一个整数,所以仍然是用不可变的值(做字典的键)。
为什么键必须是可哈希的?解释器调用哈希函数,根据字典中键的值来计算存储你的数据的位置。如果键是可变对象,它的值可改变。如果键发生变化,哈希函数会映射到不同的地址来存储数据。如果这样的情况发生,哈希函数就不可能可靠地存储或获取相关的数据。选择可哈希的键的原因就是因为它们的值不能改变。
我们知道数字和字符串可以被用做字典的键,但元组又怎么样呢?我们知道元组是不可变的,但在小节6.17.2, 我们提示过它们也可能不是一成不变的。用元组做有效的键,必须要加限制:元组中只包括像数字和字符串这样的不可变参数,才可以作为字典中有效的键。
用一个程序(userpw.py 例 7.1), 来为本章关于字典的讲述做个小结。
这个程序管理用于登录系统的用户信息:登录名字和密码。登录用户帐号建立后,已存在用户可以用登录名字和密码重返系统。新用户不能用别人的登录名建立用户帐号。【程序是有问题的,可以自己改改,只为看实际应用】
db = {}
def newuser():
prompt = 'login desired1:'
while True:
name = input(prompt)
if name in db:
prompt = 'name taken, try another'
continue
else:
break
pwd = input("passwd1:")
db[name] = pwd
def olduser():
name = input('login2:')
pwd = input('passwd2:')
passwd = db.get(name)
if passwd == pwd:
print ('welcome back',name)
else:
print ('login incorrect')
def showmenu():
prompt = """(N)ew User Login (E)xisting User Login (Q)uit Enter choice:"""
done = False
while not done:
chosen = False
while not chosen:
try:
choice = input(prompt).strip()[0].lower()
except(EOFError, KeyboardInterrupt):
choice='q'
if choice not in 'neq':
print ('invalid option, try agagin')
else:
chosen = True
done = True
newuser()
olduser()
if __name__ == "__main__":
showmenu();
数学上, 把set 称做由不同的元素组成的集合,集合(set)的成员通常被称做集合元素(setelements)。Python 把这个概念引入到它的集合类型对象里。集合对象是一组无序排列的可哈希的值。
集合成员可以做字典中的键。数学集合转为Python 的集合对象很有效。
集合(sets)有两种不同的类型:
可变集合(set) ;
不可变集合(frozenset);
对可变集合(set),你可以添加和删除元素,对 不可变集合(frozenset)则不允许这样做。
可变集合(set)不是可哈希的,因此既不能用做字典的键也不能做其他集合中的元素;
不可变集合(frozenset)则正好相反,即,他们有哈希值,能被用做字典的键或是作为集合中的一个成员;
集合通过集合(sets)模块来创建,并通过ImmutableSet类和Set 类进行访问。大家都认为把它们作为内建的数据类型是个更好的主意,因此这些类被用C 重写改进后包含进Python2.4。关于集合类型和这些类改进的更多内容,可阅读此文获得详情:PEP 218,链接地址: http://python.org/peps/pep-0218.html.
虽然现在集合类型已经是Python 的基本数据类型了,但它经常会以用户自定义类的形式出现在各种Python 程序中,就像复数一样,这样重复的劳动已数不胜数了。在现在的Python 版本之前,(即使集合类型对许多人的程序来说并不是最理想的数据结构,)许多人仍然试图给列表和字典这样的Python 标准类型添加集合功能,这样可以把它们作为真正集合类型的代理来使用。因此现在的使用者有包括“真正”集合类型在内的多种选择。
先理解Python 中的一些数学符号 ,表 7.3 集合操作符和关系符号:
如何创建集合类型和给集合赋值
集合与列表( [ ] )和字典( { } ) 不同,没有特别的语法格式。列表和字典可以分别用他们自己的工厂方法 list() 和 dict() 创建,这也是集合被创建的唯一方法 - 用集合的工厂方法 set()和 frozenset():
>>> s = set('cheeseshop')
>>> s
set(['c', 'e', 'h', 'o', 'p', 's'])
>>> t = frozenset('bookshop')
>>> t
frozenset(['b', 'h', 'k', 'o', 'p', 's'])
>>> type(s)
<type 'set'>
>>> type(t)
<type 'frozenset'>
>>> len(s)
6
>>> len(s) == len(t)
True
>>> s == t
False
如何访问集合中的值
你可以遍历查看集合成员或检查某项元素是否是一个集合中的成员:
>>> 'k' in s
False
>>> 'k' in t
True
>>> 'c' not in t
True
>>> for i in s:
... print i
...
c
e
h
o
p
s
如何更新集合
用各种集合内建的方法和操作符添加和删除集合的成员:
>>> s.add('z')
>>> s
set(['c', 'e', 'h', 'o', 'p', 's', 'z'])
>>> s.update('pypi')
>>> s
set(['c', 'e', 'i', 'h', 'o', 'p', 's', 'y', 'z'])
>>> s.remove('z')
>>> s
set(['c', 'e', 'i', 'h', 'o', 'p', 's', 'y'])
>>> s -= set('pypi')
>>> s
set(['c', 'e', 'h', 'o', 's'])
我们之前提到过,只有可变集合能被修改。试图修改不可变集合会引发异常。
>>> t.add('z')
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AttributeError: 'frozenset' object has no attribute 'add'
如何删除集合中的成员和集合
前面我们看到如何删除集合成员。如果如何删除集合本身,可以像删除任何Python 对象一样,令集合超出它的作用范围,或调用del 将他们直接清除出当前的名字空间。如果它的引用计数为零,也会被标记以便被垃圾回收。
>>> del s
成员关系 (in, not in)
就序列而言,Python 中的in 和not in 操作符决定某个元素是否是一个集合中的成员。
>>> s = set('cheeseshop')
>>> t = frozenset('bookshop')
>>> 'k' in s
False
>>> 'k' in t
True
>>> 'c' not in t
True
集合等价/不等价
等价/不等价被用于在相同或不同的集合之间做比较。两个集合相等是指,对每个集合而言,当且仅当其中一个集合中的每个成员同时也是另一个集合中的成员。
你也可以说每个集合必须是另一个集合的一个子集, 即,s <= t 和 s >= t 的值均为真(True),或(s <= t and s>= t) 的值为真(True)。集合等价/不等价与集合的类型或集合成员的顺序无关,只与集合的元素有关。
>>> s == t
False
>>> s != t
True
>>> u = frozenset(s)
>>> s == u
True
>>> set('posh') == set('shop')
True
子集/超集
Sets 用Python 的比较操作符检查某集合是否是其他集合的超集或子集。Sets 支持严格( < )子集和非严格 ( <= ) 子集, 也支持严格( > )超集和非严格 ( >= )超集。
>>> set('shop') < set('cheeseshop')
True
>>> set('bookshop') >= set('shop')
True
联合( | )
联合(union)操作和集合的OR其实是等价的,两个集合的联合是一个新集合,该集合中的每个元素都至少是其中一个集合的成员,即,属于两个集合其中之一的成员。联合符号有一个等价的方法,union().
>>> s | t
set(['c', 'b', 'e', 'h', 'k', 'o', 'p', 's'])
交集( & )
可以把交集操作比做集合的AND(或合取)操作。两个集合的交集是一个新集合,该集合中的每个元素同时是两个集合中的成员。交集符号有一个等价的方法,intersection().
>>> s & t
set(['h', 's', 'o', 'p']
差补/相对补集( – )
两个集合(s 和t)的差补或相对补集是指一个集合C,该集合中的元素,只属于集合s,而不属于集合t。差符号有一个等价的方法,difference().
>>> s - t
set(['c', 'e'])
对称差分( ^ )
和其他的布尔集合操作相似,对称差分是集合的XOR(又称”异或“ (exclusive disjunction)).两个集合(s 和t)的对称差分是指另外一个集合C,该集合中的元素,只能是属于集合s 或者集合t的成员,不能同时属于两个集合。对称差分有一个等价的方法,symmetric_difference().
>>> s ^ t
set(['k', 'b', 'e', 'c'])
混合集合类型操作
上面的示例中,左边的s 是可变集合,而右边的t 是一个不可变集合. 注意上面使用集合操作运算符所产生的仍然是可变集合,但是如果左右操作数的顺序反过来,结果就不一样了:
>>> t | s
frozenset(['c', 'b', 'e', 'h', 'k', 'o', 'p', 's'])
>>> t ^ s
frozenset(['c', 'b', 'e', 'k'])
>>> t - s frozenset(['k', 'b'])
如果左右两个操作数的类型相同,既都是可变集合或不可变集合, 则所产生的结果类型是相同的,但如果左右两个操作数的类型不相同(左操作数是set,右操作数是frozenset,或相反情况),则所产生的结果类型与左操作数的类型相同。
注意,加号不是集合类型的运算符:
>>> v = s + t
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for +: 'set' and 'set'
>>> v = s | t
>>> v
set(['c', 'b', 'e', 'h', 'k', 'o', 'p', 's'])
>>> len(v)
8
>>> s < v
True
(Union) Update ( |= )
这个更新方法从已存在的集合中添加(可能多个)成员,此方法和update()等价.
>>> s = set('cheeseshop')
>>> u = frozenset(s)
>>> s |= set('pypi')
>>> s
set(['c', 'e', 'i', 'h', 'o', 'p', 's', 'y'])
保留/交集更新( &= )
保留(或交集更新)操作保留与其他集合的共有成员。此方法和intersection_update()等价.
>>> s = set(u)
>>> s &= set('shop')
>>> s
set(['h', 's', 'o', 'p'])
差更新 ( –= )
对集合s 和t 进行差更新操作s-=t,差更新操作会返回一个集合,该集合中的成员是集合s 去除掉集合t 中元素后剩余的元素。此方法和difference_update()等价.
>>> s = set(u)
>>> s -= set('shop')
>>> s
set(['c', 'e'])
对称差分更新( ^= )
对集合s 和t 进行对称差分更新操作(s^=t),对称差分更新操作会返回一个集合,该集合中的成员仅是原集合s 或仅是另一集合t 中的成员。此方法和symmetric_difference_update()等价.
>>> s = set(u)
>>> t = frozenset('bookshop')
>>> s ^= t
>>> s
set(['c', 'b', 'e', 'k'])
len()
把集合作为参数传递给内建函数len(),返回集合的基数(或元素的个数)。
>>> s = set(u)
>>> s
set(['p', 'c', 'e', 'h', 's', 'o'])
>>> len(s)
6
set() and frozenset()
set()和frozenset()工厂函数分别用来生成可变和不可变的集合。如果不提供任何参数,默认会生成空集合。如果提供一个参数,则该参数必须是可迭代的,即,一个序列,或迭代器,或支持迭代的一个对象,例如:一个文件或一个字典。
>>> set()
set([])
>>> set([])
set([])
>>> set(())
set([])
>>> set('shop')
set(['h', 's', 'o', 'p'])
>>>
>>> frozenset(['foo', 'bar'])
frozenset(['foo', 'bar'])
>>>
>>> f = open('numbers', 'w')
>>> for i in range(5):
... f.write('%d\n' % i)
...
>>> f.close()
>>> f = open('numbers', 'r')
>>> set(f)
set(['0\n', '3\n', '1\n', '4\n', '2\n'])
>>> f.close()
我们已看到很多和内建方法等价的操作符,表 7.4 做了小结:内建方法copy() 没有等价的操作符。和同名的字典方法一样,copy()方法比用像 set(),frozenset(), 或dict()这样的工厂方法复制对象的副本要快。
表 7.5 总结了所有可变集合的内建方法,和上面的方法相似,我们已经看过许多和它们等价的操作符。
新的方法有 add(), remove(), discard(), pop(), clear(). 这些接受对象的方法,参数必须是可哈希的。
很多内建的方法几乎和操作符等价。我们说"几乎等价",意思是它们间是有一个重要区别: 当用操作符时,操作符两边的操作数必须是集合。 在使用内建方法时,对象也可以是迭代类型的。为什么要用这种方式来实现呢? Python 的文档里写明: 采用易懂的set('abc').intersection('cbs') 可以避免用 set('abc') [and] 'cbs' 这样容易出错的构建方法。
表 7.5 可变集合类型的方法
集合类型总结表
表 7.6 中,我们总结了所有的集合类型的操作符、函数和方法
集合(set)模块从 2.3 版本引进,可继承Set 或 ImmuteablSet 来生成子类。虽然从Python2.4起使用集合类型,但是集合模块不会弃用。
表7.6 集合类型操作符、函数和方法
以下是一些你可能认为有用的在线参考文章:
http://en.wikipedia.org/wiki/Set
http://www.geocities.com/basicmathsets/set.html
http://www.math.uah.edu/stat/foundations/Sets.xhtml