在学习了列表和元组之后,咱们再来学习一种容器型的数据类型,它的名字叫调集(set)。说到调集这个词咱们一定不会陌生,在数学课本上就有这个概念。假如咱们把一定范围的、确认的、能够差异的事物当作一个全体来看待,那么这个全体便是调集,调集中的各个事物称为调集的元素。通常,调集需求满足以下特性:

  1. 无序性:一个调集中,每个元素的位置都是相同的,元素之间是无序的。
  2. 互异性:一个调集中,任何两个元素都是不相同的,即元素在调集中只能呈现一次。
  3. 确认性:给定一个调集和一个恣意元素,该元素要么属这个调集,要么不归于这个调集,二者必居其一,不允许有模棱两可的情况呈现。

Python 程序中的调集跟数学上的调集没有什么本质差异,需求着重的是上面所说的无序性和互异性。无序性阐明调集中的元素并不像列中的元素那样存在某种次序,能够经过索引运算就能拜访恣意元素,调集并不支撑索引运算。另外,调集的互异性决议了调集中不能有重复元素,这一点也是调集差异于列表的当地,咱们无法将重复的元素增加到一个调集中。调集类型必然是支撑innot in成员运算的,这样就能够确认一个元素是否归于调集,也便是上面所说的调集的确认性。调集的成员运算在性能上要优于列表的成员运算,这是调集的底层存储特性决议的,此处咱们暂时不做讨论,咱们记住这个结论即可。

阐明:调集底层运用了哈希存储(散列存储),对哈希存储感兴趣的读者能够看看维基百科上“散列表”这个词条。

创立调集

在 Python 中,创立调集能够运用{}字面量语法,{}中需求至少有一个元素,由于没有元素的{}并不是空调集而是一个空字典,字典类型咱们会在下一节课中为咱们介绍。当然,也能够运用 Python 内置函数set来创立一个调集,精确的说set并不是一个函数,而是创立调集对象的构造器,这个知识点会在后边讲解面向对象编程的当地为咱们介绍。咱们能够运用set函数创立一个空调集,也能够用它将其他序列转换成调集,例如:set('hello')会得到一个包含了4个字符的调集(重复的字符l只会在调集中呈现一次)。除了这两种办法,还能够运用生成式语法来创立调集,就像咱们之前用生成式语法创立列表那样。

set1 = {1, 2, 3, 3, 3, 2}
print(set1)  # {1, 2, 3}
set2 = {True, False, True, True, False}
print(set2)  # {False, True}
set3 = set('hello')
print(set3)  # {'l', 'o', 'e', 'h'}
set4 = set([1, 2, 2, 3, 3, 3, 2, 1])
print(set4)  # {1, 2, 3}
set5 = {num for num in range(1, 20) if num % 3 == 0 or num % 7 == 0}
print(set5)  # {3, 6, 7, 9, 12, 14, 15, 18}
set6 = {('骆昊', 43), ('王大锤', 18)}
print(set6)  # {('王大锤', 18), ('骆昊', 43)}

需求提示咱们,调集中的元素有必要是hashable类型,运用哈希存储的容器都会对元素提出这一要求。所谓hashable类型指的是能够计算出哈希码的数据类型,通常不可变类型都是hashable类型,如整数(int)、浮点小数(float)、布尔值bool)、字符串(str)、元组(tuple)等。可变类型都不是hashable类型,由于可变类型无法计算出确认的哈希码,所以它们不能放到调集中。例如:咱们不能将列表作为调集中的元素;同理,由于调集本身也是可变类型,所以调集也不能作为调集中的元素。咱们能够创立出嵌套的列表,可是咱们不能创立出嵌套的调集,这一点在运用调集的时候一定要引起注意。

调集的遍历

咱们能够经过len函数来取得调集中有多少个元素,可是咱们不能经过索引运算来遍历调集中的元素,由于调集元素并没有特定的次序。当然,要完成对调集元素的遍历,咱们依然能够运用for-in循环,代码如下所示。

set1 = {'Python', 'C++', 'Java', 'Kotlin', 'Swift'}
for elem in set1:
    print(elem)

提示:咱们看看上面代码的运转结果,经过单词输出的次序体会一下调集的无序性。

调集的运算

Python 为调集类型提供了十分丰富的运算符,主要包括:成员运算、交集运算、并集运算、差集运算、比较运算(持平性、子集、超集)等。

成员运算

能够经过成员运算innot in 查看元素是否在调集中,代码如下所示。

set1 = {11, 12, 13, 14, 15}
print(10 in set1)      # False 
print(15 in set1)      # True
set2 = {'Python', 'Java', 'C++', 'Swift'}
print('Ruby' in set2)  # False
print('Java' in set2)  # True

二元运算

调集的二元运算主要指调集的交集、并集、差集、对称差等运算,这些运算能够经过运算符来完成,也能够经过调集类型的办法来完成,代码如下所示。

从零开始学Python第12课:常用数据结构之集合

set1 = {1, 2, 3, 4, 5, 6, 7}
set2 = {2, 4, 6, 8, 10}
# 交集
print(set1 & set2)                      # {2, 4, 6}
print(set1.intersection(set2))          # {2, 4, 6}
# 并集
print(set1 | set2)                      # {1, 2, 3, 4, 5, 6, 7, 8, 10}
print(set1.union(set2))                 # {1, 2, 3, 4, 5, 6, 7, 8, 10}
# 差集
print(set1 - set2)                      # {1, 3, 5, 7}
print(set1.difference(set2))            # {1, 3, 5, 7}
# 对称差
print(set1 ^ set2)                      # {1, 3, 5, 7, 8, 10}
print(set1.symmetric_difference(set2))  # {1, 3, 5, 7, 8, 10}

经过上面的代码能够看出,对两个调集求交集,&运算符和intersection办法的效果是完全相同的,运用运算符的办法明显更直观且代码也更简短。需求阐明的是,调集的二元运算还能够跟赋值运算一同构成复合赋值运算,例如:set1 |= set2相当于set1 = set1 | set2,跟|=效果相同的办法是updateset1 &= set2相当于set1 = set1 & set2,跟&=效果相同的办法是intersection_update,代码如下所示。

set1 = {1, 3, 5, 7}
set2 = {2, 4, 6}
set1 |= set2
# set1.update(set2)
print(set1)  # {1, 2, 3, 4, 5, 6, 7}
set3 = {3, 6, 9}
set1 &= set3
# set1.intersection_update(set3)
print(set1)  # {3, 6}
set2 -= set1
# set2.difference_update(set1)
print(set2)  # {2, 4}

比较运算

两个调集能够用==!=进行持平性判别,假如两个调集中的元素完全相同,那么==比较的结果便是True,否则便是False。假如调集A的恣意一个元素都是调集B的元素,那么调集A称为调集B的子集,即对于∀a∈A\small{\forall{a} \in {A}},均有a∈B\small{{a} \in {B}},则A⊆B\small{{A} \subseteq {B}}AB的子集,反过来也能够称BA的超集。假如AB的子集且A不等于B,那么A便是B的真子集。Python 为调集类型提供了判别子集和超集的运算符,其实便是咱们十分熟悉的<<=>>=这些运算符。当然,咱们也能够经过调集类型的办法issubsetissuperset来判别调集之间的联系,代码如下所示。

set1 = {1, 3, 5}
set2 = {1, 2, 3, 4, 5}
set3 = {5, 4, 3, 2, 1}
print(set1 < set2)   # True
print(set1 <= set2)  # True
print(set2 < set3)   # False
print(set2 <= set3)  # True
print(set2 > set1)   # True
print(set2 == set3)  # True
print(set1.issubset(set2))    # True
print(set2.issuperset(set1))  # True

阐明:上面的代码中,set1 < set2判别set1是不是set2的真子集,set1 <= set2判别set1是不是set2的子集,set2 > set1判别set2是不是set1的超集。当然,咱们也能够经过set1.issubset(set2)判别set1是不是set2的子集;经过set2.issuperset(set1)判别set2是不是set1的超集。

调集的办法

刚才咱们说过,Python 中的调集是可变类型,咱们能够经过调集类型的办法向调集增加元素或从调集中删去元素。

set1 = {1, 10, 100}
# 增加元素
set1.add(1000)
set1.add(10000)
print(set1)  # {1, 100, 1000, 10, 10000}
# 删去元素
set1.discard(10)
if 100 in set1:
    set1.remove(100)
print(set1)  # {1, 1000, 10000}
# 清空元素
set1.clear()
print(set1)  # set()

阐明:删去调集元素的remove办法在元素不存在时会引发KeyError错误,所以上面的代码中咱们先经过成员运算判别元素是否在调集中。调集类型还有一个pop办法能够从调集中随机删去一个元素,该办法在删去元素的同时会取得被删去的元素,而removediscard办法仅仅是删去元素,不会取得被删去的元素。

调集类型还有一个名为isdisjoint的办法能够判别两个调集有没有相同的元素,假如没有相同元素,该办法回来True,否则该办法回来False,代码如下所示。

set1 = {'Java', 'Python', 'C++', 'Kotlin'}
set2 = {'Kotlin', 'Swift', 'Java', 'Dart'}
set3 = {'HTML', 'CSS', 'JavaScript'}
print(set1.isdisjoint(set2))  # False
print(set1.isdisjoint(set3))  # True

不可变调集

Python 中还有一种不可变类型的调集,名字叫frozensetsetfrozenset的差异就好像listtuple的差异,frozenset由所以不可变类型,能够计算出哈希码,因此它能够作为set中的元素。除了不能增加和删去元素,frozenset在其他方面跟set是一样的,下面的代码简单的展现了frozenset的用法。

fset1 = frozenset({1, 3, 5, 7})
fset2 = frozenset(range(1, 6))
print(fset1)          # frozenset({1, 3, 5, 7})
print(fset2)          # frozenset({1, 2, 3, 4, 5})
print(fset1 & fset2)  # frozenset({1, 3, 5})
print(fset1 | fset2)  # frozenset({1, 2, 3, 4, 5, 7})
print(fset1 - fset2)  # frozenset({7})
print(fset1 < fset2)  # False

总结

Python 中的调集类型是一种无序容器不允许有重复运算,由于底层运用了哈希存储,调集中的元素有必要是hashable类型。调集与列表最大的差异在于调集中的元素没有次序、所以不能够经过索引运算拜访元素、可是调集能够执行交集、并集、差集等二元运算,也能够经过联系运算符查看两个调集是否存在超集、子集等联系。