3.4 集合:元素之间不允许重复
集合(set)属于Python无序可变序列,使用一对大括号作为定界符,元素之间使用逗号分隔,同一个集合内的每个元素都是唯一的,元素之间不允许重复。
集合中只能包含数字、字符串、元组等不可变类型(或者说可哈希)的数据,而不能包含列表、字典、集合等可变类型的数据。Python提供了一个内置函数hash()来计算对象的哈希值,凡是无法计算哈希值(调用内置函数hash()时抛出异常)的对象都不能作为集合的元素,也不能作为字典对象的“键”。
3.4.1 集合对象的创建与删除
直接将集合赋值给变量即可创建一个集合对象。
>>>a={3,5} #创建集合对象 >>>type(a) #查看对象类型 <class'set'>
也可以使用set()函数将列表、元组、字符串、range对象等其他可迭代对象转换为集合,如果原来的数据中存在重复元素,则在转换为集合的时候只保留一个;如果原序列或迭代对象中有不可哈希的值,无法转换成为集合,抛出异常。
>>>a_set=set(range(8,14)) #把range对象转换为集合 >>>a_set {8,9,10,11,12,13} >>>b_set=set([0,1,2,3,0,1,2,3,7,8]) #转换时自动去掉重复元素 >>>b_set {0,1,2,3,7,8} >>>x=set() #空集合
除了列表推导式、生成器推导式、字典推导式之外,Python还支持使用集合推导式来快速生成集合。
>>>{x.strip() for x in ('he', 'she ', ' I')} {'I', 'she', 'he'} >>>import random >>>x={random.randint(1,500) for i in range(100)} #生成随机数,自动去除重复元素 >>>len(x) #一般而言输出结果会小于100 >>>{str(x) for x in range(10)} {'3', '0', '1', '8', '4', '7', '5', '6', '9', '2'}
当不再使用某个集合时,可以使用del命令删除整个集合。
3.4.2 集合操作与运算
1.集合元素增加与删除
集合对象的add()方法可以增加新元素,如果该元素已存在则忽略该操作,不会抛出异常;update()方法合并另外一个集合中的元素到当前集合中,并自动去除重复元素。
>>>s={1,2,3} >>>s.add(3) #添加元素,重复元素自动忽略 >>>s.update({3,4}) #更新当前字典,自动忽略重复的元素 >>>s {1,2,3,4}
集合对象的pop()方法随机删除并返回集合中的一个元素,如果集合为空则抛出异常;remove()方法删除集合中的元素,如果指定元素不存在则抛出异常;discard()方法从集合中删除一个特定元素,如果元素不在集合中则忽略该操作;clear()方法清空集合。
>>>s.discard(5) #删除元素,不存在则忽略该操作 >>>s.remove(5) #删除元素,不存在就抛出异常 KeyError: 5 >>>s.pop() #删除并返回一个元素 1
2.集合运算
内置函数len()、max()、min()、sum()、sorted()、map()、filter()、enumerate()等也适用于集合。另外,Python集合还支持数学意义上的交集、并集、差集等运算。
>>>a_set=set([8,9,10,11,12,13]) >>>b_set={0,1,2,3,7,8} >>>a_set|b_set #并集 {0,1,2,3,7,8,9,10,11,12,13} >>>a_set.union(b_set) #并集 {0,1,2,3,7,8,9,10,11,12,13} >>>a_set&b_set #交集 {8} >>>a_set.intersection(b_set) #交集 {8} >>>a_set.difference(b_set) #差集 {9,10,11,12,13} >>>a_set-b_set {9,10,11,12,13} >>>a_set.symmetric_difference(b_set) #对称差集 {0,1,2,3,7,9,10,11,12,13} >>>a_set^b_set {0,1,2,3,7,9,10,11,12,13} >>>x={1,2,3} >>>y={1,2,5} >>>z={1,2,3,4} >>>x<y #比较集合大小/包含关系 False >>>x<z #真子集 True >>>y<z False >>>{1,2,3}<={1,2,3} #子集 True >>>x.issubset(y) #测试是否为子集 False >>>x.issubset(z) True >>>{3}&{4} set() >>>{3}.isdisjoint({4}) #如果两个集合的交集为空,返回True True
需要注意的是,关系运算符>、>=、<、<=作用于集合时表示集合之间的包含关系,而不是比较集合中元素的大小关系。对于两个集合A和B,如果A<B不成立,不代表A>=B就一定成立。
3.4.3 不可变集合frozenset
Python内置支持frozenset类,用法与set类基本相似,支持交集、并集、差集等运算以及测试是否为子集或超集等运算。与set类不同的是,frozenset是不可变集合,没有提供add()、remove()等可以修改集合对象的方法。
>>>x=frozenset(range(5)) #创建不可变集合 >>>x frozenset({0,1,2,3,4}) >>>x.add(5) #不支持add()方法,抛出异常 AttributeError: 'frozenset'object has no attribute'add' >>>x|frozenset(range(5,10)) #并集运算 frozenset({0,1,2,3,4,5,6,7,8,9}) >>>x&frozenset(range(5,10)) #交集运算 frozenset() >>>x-frozenset(range(5,10)) #差集运算 frozenset({0,1,2,3,4}) >>>frozenset(range(4))<frozenset(range(5)) #集合包含关系比较 True
3.4.4 集合应用案例
The Zen of Python认为There should be one—and preferably only one—obvious way to do it。编写代码时除了要准确地实现功能之外,还要考虑代码的优化,尽量找到一种更快、更好的方法实现预定功能。Python字典和集合都使用hash表来存储元素,元素查找速度非常快,关键字in作用于字典和集合时比作用于列表要快得多。
import random import time x1=list(range(10000)) x2=tuple(range(10000)) x3=set(range(10000)) x4=dict(zip(range(10000), range(10000))) r=random.randint(0,9999) for t in (x4, x3, x2, x1): start=time.time() for i in range(9999999): r in t print(type(t), 'time used:', time.time()-start)
从下面的运行结果可以看出,对于成员测试运算符in,列表的效率远远不如字典和集合,并且随着序列的变长,列表的查找速度越来越慢,而字典和集合基本上不受影响。
<class'dict'>time used: 1.1570661067962646 <class'set'>time used: 1.442082405090332 <class'tuple'>time used: 1185.4768052101135 <class'list'>time used: 1183.18967461586
作为集合的具体应用,可以使用集合快速提取序列中单一元素,即提取出序列中所有不重复的元素。如果使用传统方式的话,需要编写下面的代码:
>>>import random #生成100个介于0~9999之间的随机数 >>>listRandom=[random.choice(range(10000)) for i in range(100)] >>>noRepeat=[] >>>for i in listRandom : if i not in noRepeat : noRepeat.append(i)
而如果使用集合的话,只需要下面这么一行代码就可以了。
>>>newSet=set(listRandom)
集合中的元素不允许重复,Python集合的内部实现为此做了大量相应的优化,添加元素时如果已经存在则自动忽略。下面的代码用于返回指定范围内一定数量的不重复数字。
import random def randomNumbers(number, start, end): ''’使用集合来生成number个介于start和end之间的不重复随机数’'' data=set() while len(data)<number: element=random.randint(start, end) data.add(element) return data
当然,如果在项目中需要这样一个功能的时候,还是直接使用random模块的sample()函数更好一些。但random模块的sample()函数只支持列表、元组、集合、字符串和range对象,不支持字典以及map、zip、enumerate、filter等惰性求值的迭代对象。
>>>import random >>>random.sample(range(1000),20) #在指定分布中选取不重复元素 [61,538,873,815,708,609,995,64,7,719,922,859,807,464,789,651,31,702,504,25]
下面的两段代码用来测试指定列表中是否包含非法数据,很明显第二段使用集合的代码更高效一些。
import random lstColor=('red', 'green', 'blue') colors=[random.choice(lstColor) for i in range(10000)] for item in colors: #遍历列表中的元素并逐个判断 if item not in lstColor: print('error:', item) break if (set(colors)-set(lstColor)): #转换为集合之后再比较 print('error')
下面的代码使用字典和集合模拟了有向图结构,并实现了节点的入度和出度计算,代码中所用的有向图如图3-3所示。
图3-3 有向图结构
def getDegrees(orientedGraph, node): outDegree=len(orientedGraph.get(node, [])) inDegree=sum(1 for v in orientedGraph.values() if node in v) return (inDegree, outDegree) graph={'a':set('bcdef'), 'b':set('ce'), 'c':set('d'), 'd':set('e'), 'e':set('f'), 'f':set('cgh'), 'g':set('fhi'), 'h':set('fgi'), 'i':set()} print(getDegrees(graph, 'h'))