12. 数据类型 - 集合详解

茶桁的 AI 秘籍 Python-set

Hi, 大家好。我是茶桁

通过最近几节课的内容,我们已经了解到了大部分的容器类数据的特性和应用,今天这一节课是容器类数据的最后一部分。让我们今天来详细了解一下「集合」。

集合是确定的一组无序的数据的组合。注意这一句话中的几个概念:

  • 首先是「确定的」,当前集合中的元素的值是不能重复的。
  • 集合是由多个数据组合的容器类型数据
  • 集合中的数据没有先后顺序
  • 集合的作用大多数时候是为了从成员检测、从无序列中去除重复项。还有就是数学中的集合类计算,例如交集、并集、差集一集对称差集等等。

集合的定义

集合的定义和字典类数据的定义非常像,包含了三种定义方式:

  • 可以直接使用{}来定义集合
  • 可以使用set()进行集合的定义和转换
  • 使用集合推导式来完成集合的定义

⚠️ 需要注意:集合中的元素不能重复,集合中存放的数据为:Number, String, Tuple,冰冻集合

冰冻集合

在集合的定义部分,其他数据类型我们都能理解,唯独多出来一个冰冻集合似乎没有见过,也难以理解。

冰冻集合的定义,需要且仅能使用frozenset()函数来进行定义。故名思义,冰冻集合一旦定义之后,是不能进行修改的,只能做一些集合相关的运算,比如交集,差集等等。

回过头来看冰冻集合的定义函数frozenset(), 这个函数本身是一个强制转换类的函数,可以把其他任何容器类型的数据转为冰冻集合,然后参与集合运算。

1
2
3
4
5
6
7
8
9
# 定义一个冰冻集合
mySets = frozenset({'love', 666, 333, 2, 'a', 1, 2, 'MAMT','55IW'})

# 遍历集合
for i in mySets:
print(i, end=", ")

---
1, 2, 55IW, love, 333, MAMT, 666, a,

也是可以看到,打印的结果完全没有任何顺序。

冰冻集合当然也可以使用集合推导式:

1
2
3
4
5
res = frozenset({i<<1 for i in range(6)})
print(res)

---
frozenset({0, 2, 4, 6, 8, 10})

可以进行拷贝:

1
2
3
4
5
res = res.copy()
print(res)

---
frozenset({0, 2, 4, 6, 8, 10})

当然冰冻集合也可以进行集合的运算,不过这部分我们将在后面讲解集合的时候来学习。暂时我们只是对「冰冻集合」的概念有个了解就可以了。

集合的基本操作和常规函数

以往的几节,我们都是将集合的操作和函数分开来讲,而这次我们放在一起讲。其实也没其他原因,就是因为这部分的内容并没有多少,并且很容易理解。

1
2
3
4
5
6
# 定义集合
mySets = {123,'abc',False,'love',True,(1,2,3),0,3.1415,'123',1}
mySets

---
{(1, 2, 3), 123, '123', 3.1415, False, True, 'abc', 'love'}

打印的结果再一次验证了集合无序,除此之外,我们可以看到打印出来的集合比我们进行定义的时候似乎少了,这又是为什么呢?

原来,在集合内布尔类型的数据其实就是 0 和 1, True 表示为 1, False 表示为 0,而集合内的值是不能重复的,所以,布尔值和0,1就只能存在一个。

我们来尝试检测一下集合中的值,和其他容器类数据一样,我们可以直接使用for...in

1
2
3
4
5
6
7
# 检测集合中的值
print('123' in mySets)
print('123' not in mySets)

---
True
False

然后一样,可以使用len()检测长度:

1
2
3
4
print(len(mySets))

---
8

遍历的方法依然是用for

1
2
3
4
5
6
7
8
9
10
11
12
for i in mySets:
print(i, type(i))

---
False <class 'bool'>
True <class 'bool'>
3.1415 <class 'float'>
(1, 2, 3) <class 'tuple'>
love <class 'str'>
abc <class 'str'>
123 <class 'int'>
123 <class 'str'>

为什么我这次会将数据类型也打印出来呢?是因为我想让大家好好记住这些类型,目前集合就只支持这些数据类型,其他的并不支持放入。比如说列表,是无法进入集合内的:

1
2
3
4
5
# 看看列表是否能放入
mySets = {123,'abc',False,'love',True,(1,2,3),0,3.1415,'123',1,['list', 2, 3, 4, 5]}

---
TypeError: unhashable type: 'list'

可以看到报错信息提示,不支持类型:列表。

那我们如何像集合中追加元素呢?可以使用add()

1
2
3
4
5
6
7
8
# 定义集合
mySets = {123,'abc',False,'love',True,(1,2,3),0,3.1415,'123',1}
res = mySets.add('茶桁')
print(mySets, '\nres:', res)

---
{False, True, 3.1415, (1, 2, 3), 'love', '茶桁', 'abc', 123, '123'}
res: None

可以看到我们在其中追加了一个字符串茶桁, 但是我们再一次验证了集合的无序,新加入的字符串并没有和其他数据类型一样新加入的元素放在最末端。

并且我们注意到了,我用res来接收了add()的返回值,返回了一个None

除了追加之外,当然我们也可以对集合进行删除元素的处理:

1
2
3
4
5
6
res = mySets.pop()
print(mySets, '\nres:', res)

---
{True, 3.1415, (1, 2, 3), 'love', '茶桁', 'abc', 123, '123'}
res: False

pop()删除集合内的元素是随机的,并且,会将删除的元素返回。

如果想指定删除集合中的元素有没有办法呢?其实也有,remove()discard()都可以做到,但是两者又有些区别,我们接着看代码:

1
2
3
4
5
6
7
8
9
# 使用 remove()
res = mySets.remove('abc')
print(mySets)
res = mySets.remove('aaa')

---
{True, 3.1415, (1, 2, 3), 'love', '茶桁', 123, '123'}
res: None
KeyError: 'aaa'

能看到,remove()确实可以删除集合内的指定元素,并给一个返回值None。不过当集合内没有此元素的时候,就会报错,提示关键词错误。

那让我们再来看看discard()

1
2
3
4
5
6
7
8
9
10
# 使用 discard
mySets = {123,'abc',False,'love',True,(1,2,3),0,3.1415,'123',1}
res = mySets.discard('123')
print(mySets, f'res:{res}')
res = mySets.discard('aaa')
print(mySets, f'res:{res}')

---
{False, True, 3.1415, (1, 2, 3), 'love', 'abc', 123} res:None
{False, True, 3.1415, (1, 2, 3), 'love', 'abc', 123} res:None

remove()一样,也删除了一个指定元素,并且返回了None。不同的是,当我们使用discard删除一个不存在的元素时,discard虽然没有删除任何内容,但是也没有报错。

一个个删除太麻烦了,这个集合我就想让它变成一个空集合,好办,用clear()做清空处理呗,和字典一样:

1
2
3
4
5
6
7
8
mySets = {123,'abc',False,'love',True,(1,2,3),0,3.1415,'123',1}
print(mySets)
mySets.clear()
print(mySets)

---
{False, True, 3.1415, (1, 2, 3), 'love', 'abc', 123, '123'}
set()

空集合拿到了,可以放入我们喜欢的元素了。依然和字典一致,我们可以使用update:

1
2
3
4
5
6
7
8
res = mySets.update({1, 2, 3, 4, 5})
print(mySets, f'res:{res}')
res = mySets.update({2, 3, 4, 5, 6})
print(mySets, f'res:{res}')

---
{1, 2, 3, 4, 5} res:None
{1, 2, 3, 4, 5, 6} res:None

结果中显示,我们更新成功了,新添加了一些元素进入集合。那第二次添加,为什么就只有6添加进去了呢?还记得么?集合不能有重复值,就跟字典不能有重复的key一样。在字典中使用update,遇到相同key后面的value会被更新,那其实集合也是一样的,只是因为只有一个值,所以更新完不还是这个值么。

在冰冻集合的时候我们用到过一次copy, 这里我们要单独拿出来说说,因为集合中的元素都是不可变的,包括元组和冰冻集合,所以当前集合的浅拷贝并不存在深拷贝的问题。换句话说,就是不存在在拷贝后,对集合中不可变的二级容器进行操作的问题。

1
2
3
4
5
6
mySets = {123,'abc',False,'love',True,(1,2,3),0,3.1415,'123',1}
res = mySets.copy()
print(res)

---
{False, True, 3.1415, (1, 2, 3), 'love', 'abc', 123, '123'}

集合是没有deepcopy方法的。

集合的运算和检测

集合的主要运算有四种,以下将列出这四种以及他们的方法:

  • 交集 &, set.intersection(), set.intersection_update()
  • 并集 |, union(), update()
  • 差集 -, difference(), difference_update()
  • 对称差集 ^, symmetric_difference(), symmetric_difference_update()

我们先来看看符号运算:

1
2
3
# 先定义两个集合
mySet1 = {'Python','Go','Rust', 'Swift', 'C++'}
mySet2 = {'C','JavaScript', 'Ruby', 'Java', 'Python'}

然后让我们先求交集&:

1
2
3
4
5
6
# 求两个集合交集
res = mySet1 & mySet2
print(res)

---
{'Python'}

求两个集合并集(求并集的时候会去除重复项)|

1
2
3
4
5
res = mySet1 | mySet2
print(res)

---
{'Java', 'C', 'Rust', 'C++', 'Go', 'Python', 'Ruby', 'JavaScript', 'Swift'}

求两个集合差集-

1
2
3
4
5
6
7
8
res = mySet1 - mySet2
res2 = mySet2 - mySet1
print(res)
print(res2)

---
{'Go', 'Rust', 'C++', 'Swift'}
{'Java', 'C', 'Ruby', 'JavaScript'}

这段代码结果中resres2的区别在于,resmySet1中有,而mySet2中没有, res2mySet2中有,而mySet1中没有。

求两个集合对称差集^

1
2
3
4
5
res = mySet1 ^ mySet2
print(res)

---
{'Java', 'C', 'C++', 'Swift', 'Rust', 'Go', 'Ruby', 'JavaScript'}

看完符号运算,我们可以再来看看函数运算

交集的运算函数为set.intersection(), set.intersection_update(), 那这两个函数又有什么区别呢?

1
2
3
4
5
6
7
8
res = mySet1.intersection(mySet2)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
{'Python'}
mySet1:{'Go', 'Python', 'Rust', 'C++', 'Swift'},
mySet2:{'Java', 'C', 'Python', 'Ruby', 'JavaScript'}

让我们先记住intersection()的结果, mySet1mySet2并没有发生变化,而返回值为两个集合相同的内容。然后我们再来看看另外一个函数:

1
2
3
4
5
6
7
8
res = mySet1.intersection_update(mySet2)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
None
mySet1:{'Python'},
mySet2:{'Java', 'C', 'Python', 'Ruby', 'JavaScript'}

首先我们就能看到,返回值为None, 并且mySet1发生了变化。也就是说,set.intersection_update()是将两者的交集重复赋值给到了头部的变量,这里就是mySet1,然后返回一个None值。

接着我们来看一下并集运算函数: union(), update()

1
2
3
4
5
6
7
8
9
10
11
mySet1 = {'Python','Go','Rust', 'Swift', 'C++'}
mySet2 = {'C','JavaScript', 'Ruby', 'Java', 'Python'}

res = mySet1.union(mySet2)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
{'Java', 'C', 'Rust', 'C++', 'Go', 'Python', 'Ruby', 'JavaScript', 'Swift'}
mySet1:{'Go', 'Python', 'Rust', 'C++', 'Swift'},
mySet2:{'Java', 'C', 'Python', 'Ruby', 'JavaScript'}

我们首先看到了返回值,正事两个集合的并集,两个原始集合也没有发生变化。

再来看看update()

1
2
3
4
5
6
7
8
9
10
11
mySet1 = {'Python','Go','Rust', 'Swift', 'C++'}
mySet2 = {'C','JavaScript', 'Ruby', 'Java', 'Python'}

res = mySet1.update(mySet2)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
None
mySet1:{'Java', 'C', 'Rust', 'C++', 'Go', 'Python', 'Ruby', 'JavaScript', 'Swift'},
mySet2:{'Java', 'C', 'Python', 'Ruby', 'JavaScript'}

可以很明显看到区别:返回值为None,并集的计算结果被复制给了第一个变量,这里是mySet1

再看完并集之后,就轮到差集了, 分别是这两个函数difference(),difference_update()

1
2
3
4
5
6
7
8
9
10
11
mySet1 = {'Python','Go','Rust', 'Swift', 'C++'}
mySet2 = {'C','JavaScript', 'Ruby', 'Java', 'Python'}

res = mySet1.difference(mySet2)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
{'Go', 'Rust', 'C++', 'Swift'}
mySet1:{'Go', 'Python', 'Rust', 'C++', 'Swift'},
mySet2:{'Java', 'C', 'Python', 'Ruby', 'JavaScript'}

返回值为差集的计算结果, 这里是mySet1有的而mySet2没有的。那不用问,按照一贯的惯例, difference_update()一定是将计算结果返回给第一个变量,这回我们换一下,将mySet2换成第一个变量试试:

1
2
3
4
5
6
7
8
9
10
11
mySet1 = {'Python','Go','Rust', 'Swift', 'C++'}
mySet2 = {'C','JavaScript', 'Ruby', 'Java', 'Python'}

res = mySet2.difference_update(mySet1)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
None
mySet1:{'Go', 'Python', 'Rust', 'C++', 'Swift'},
mySet2:{'Java', 'C', 'Ruby', 'JavaScript'}

果然就跟料想的一样,最终的计算结果赋值给了mySet2

最后当然就是对称差集函数symmetric_difference() symmetric_difference_update()

1
2
3
4
5
6
7
8
9
10
11
mySet1 = {'Python','Go','Rust', 'Swift', 'C++'}
mySet2 = {'C','JavaScript', 'Ruby', 'Java', 'Python'}

res = mySet2.symmetric_difference(mySet1)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
{'Java', 'C', 'C++', 'Swift', 'Rust', 'Go', 'Ruby', 'JavaScript'}
mySet1:{'Go', 'Python', 'Rust', 'C++', 'Swift'},
mySet2:{'Java', 'C', 'Python', 'Ruby', 'JavaScript'}

res接收了计算结果,成为了一个新集合。

接下来,大家应该能猜到了吧?不过还是要做做实验才知道,万一和自己想的不一样呢。

1
2
3
4
5
6
7
8
9
10
11
mySet1 = {'Python','Go','Rust', 'Swift', 'C++'}
mySet2 = {'C','JavaScript', 'Ruby', 'Java', 'Python'}

res = mySet2.symmetric_difference_update(mySet1)
print(res)
print(f'mySet1:{mySet1}, \nmySet2:{mySet2}')

---
None
mySet1:{'Go', 'Python', 'Rust', 'C++', 'Swift'},
mySet2:{'Java', 'C', 'C++', 'Swift', 'Rust', 'Go', 'Ruby', 'JavaScript'}

嗯,这个结果,一点都没有惊喜和意外。

好吧,运算我们学完之后,接着我们要看看集合的检测方法, 一共有三个,记住用法就可以了:

  • issuperset()检测是否为超集
  • issubset()检测是否为子集
  • isdisjoint()检测是否不相交

为了更好的说明,我们不能在用之前的集合,这回,我们定义三个集合来看:

1
2
3
mySet1 = {1, 2, 3, 4, 5, 6}
mySet2 = {1, 2, 3}
mySet3 = {6, 7, 8}

接下来从第一个检测开始:

1
2
3
4
5
6
7
8
print(mySet1.issuperset(mySet2))
print(mySet2.issuperset(mySet1))
print(mySet1.issuperset(mySet3))

---
True
False
False

不知道大家在数学里有没有学过「超集」的概念,我们从最后的打印结果可以看出来,mySet1mySet2的超集,反过来则不是,并且,mySet1也不是mySet3的超集。观察三个集合内的元素我们可以得出结论,如果集合a是另外一个集合b的超集,那么集合b内的元素一定在集合a中都找得到。

再来检测子集:

1
2
3
4
5
6
7
8
print(mySet1.issubset(mySet2))
print(mySet2.issubset(mySet1))
print(mySet3.issubset(mySet1))

---
False
True
False

从这个结果中我们能看到,子集的概念就和超集完全相反了。

最后就是检测两个集合是否相交了,也就是集合中的元素有没有重复的:

1
2
3
4
5
6
7
8
9
10
print(mySet1.isdisjoint(mySet2))
print(mySet2.isdisjoint(mySet1))
print(mySet3.isdisjoint(mySet1))
print(mySet2.isdisjoint(mySet3))

---
False
False
False
True

虽然前三个都打印的False, 最后一个打印的True,但是我们从集合中应该知道,只有mySet2mySet3没有相交关系。所以我们可以知道,isdisjoint()这个函数其实是检测不相交的。也就是说,返回结果为False则证明相交,返回结果为True反而是不相交。

结语

至此,随着我们的集合内容讲完,咱们的容器类数据类型就全部讲完了。

咱们下一节开始,咱们要开始行的篇章。下一节内容预告:Python 中 File 文件的操作。

本节课一样就不布置作业了,大家好好的将最近将的容器类数据好好的回顾一下,将基础打扎实。

12. 数据类型 - 集合详解

https://hivan.me/Detailed-of-set/

作者

Hivan Du

发布于

2023-08-08

更新于

2024-01-16

许可协议

评论