4.集合 (Set)

2 阅读4分钟

1. 集合 (Set) 基础

set 也可以像 listtuple 一样,用来存放一组数据。使用大括号 {} 表示。list用[],tuple用(),set最重要的特点就是无序,去重。

s = {44, True, 22, 'mj'}
print(len(s))       # 4
print(44 in s)      # True
print(22 not in s)  # False

空集合与空字典

# 空集合
s1 = set()
print(type(s1))  # <class 'set'>
print(bool(s1))  # False
print(len(s1))   # 0

2. 集合的三大特点

2.1 无序

set 是可迭代类型,但它的元素没有固定的先后顺序。每次打印出来或者遍历出来的顺序可能都不一样。

s = {55, 44, 33, 22}
print(s)  # {33, 44, 22, 55} (每次运行可能不同)
for i in s:
    print(i)

2.2 无索引

set 中的元素不能通过索引访问

s = {55, 44, 33, 22, 11}
# print(s[1])  # TypeError: 'set' object is not subscriptable

2.3 去重

set 自带去重能力,集合中不会存在两个值相等的元素。

s = {55, 44, 55, 33, 44, 11, 55}
print(len(s))  # 4
print(s)       # {33, 11, 44, 55} (自动去除了重复的元素)

3. 集合推导式

和列表、字典一样,集合也可以使用推导式快速生成。

s1 = {i * 11 for i in range(1, 6)}
print(s1)  # {33, 11, 44, 22, 55}

names = ['乔峰', '张无忌', '杨过', '郭靖', '杨戬', '张三丰']
s3 = {name[0] for name in names}
print(s3)  # {'乔', '郭', '张', '杨'} (自动去重)

# 也可以用 set() 配合生成器表达式
s2 = set(i * 11 for i in range(1, 6))

4. 可迭代 (Iterable) vs 序列类型 (Sequence)

  • 可迭代:可以逐个访问内部的元素。
  • 序列类型:元素之间有严格的先后顺序。

通常情况下:

  • 序列类型都是可迭代的。
  • 可迭代的不一定是序列类型(例如 set 是可迭代的,但不是序列类型)。
数据类型listtuplestrrangesetintfloatbool
序列类型
可迭代

5. set 和序列类型之间的转换

可以通过 set()list()tuple() 进行相互转换,字符串也可以使用 join()

s1 = set([55, 44, 44, 33])   # {33, 44, 55} (列表转集合,自动去重)
s2 = set((55, 33, 33, 22))   # {33, 22, 55} (元组转集合)
s3 = set('mj666')            # {'6', 'm', 'j'} (字符串转集合)

s4 = list({55, 44, 33, 22})  # [33, 44, 22, 55] (集合转列表)
s5 = tuple({55, 44, 33, 22}) # (33, 44, 22, 55) (集合转元组)

s6 = '_'.join({'5', '4', '3', '2'}) # '2_4_5_3' (结合 join 使用)

6. set 的常用方法

set 是可变类型,可以动态添加、删除元素。

添加元素

  • s.add(x):添加元素 x
s = set()
s.add(55)
s.add(44)
s.add(33)
s.add(44)  # 再次添加 44 会被去重
print(s)   # {33, 44, 55}

删除元素

  • s.discard(x):删除元素 x。如果 x 不存在,不会报错
  • s.remove(x):删除元素 x。如果 x 不存在,会报错 (KeyError)
  • s.pop():随机删除并返回 1 个元素。如果集合为空,会报错 (KeyError)
  • s.clear():清空集合中的所有元素。
s = {55, 44, 33, 22}

s.discard(44)  # {33, 22, 55}
s.discard(66)  # 元素不存在,啥也不做,不报错

s.remove(22)   # 移除 22
# s.remove(66) # 报错 KeyError: 66

s.pop()        # 随机弹出一个元素并返回
s.clear()      # 清空集合

7. 进阶:可哈希 (Hashable) 与 不可哈希 (Unhashable)

set 中的元素必须都是可哈希的。不可哈希的对象无法存放到 set 中。

# 报错 TypeError: unhashable type: 'list'
# s = { 100, False, 3.14, 'mj', (10, 20, 30), [44, 55, 66] }

如何判断一种类型是否可哈希?

通过查看类型的 __hash__ 属性:

  • 如果 类型.__hash__None,说明不可哈希
  • 如果 类型.__hash__ 不为 None,说明可哈希
print(int.__hash__)   # <slot wrapper '__hash__' of 'int' objects>
print(tuple.__hash__) # <slot wrapper '__hash__' of 'tuple' objects>

print(set.__hash__)   # None
print(list.__hash__)  # None
print(dict.__hash__)  # None

总结:

  • 不可变类型 往往是可哈希的 (如 int, float, bool, str, tuple),可以放入集合。
  • 可变类型 往往是不可哈希的 (如 list, dict, set),不能放入集合中。

为什么set规定元素必须是可哈希的呢?

set的查找和去重速度极快,哈希想象成一台指纹提取机,同样的数据,每次算出来的哈希值必须是一模一样的,不同的数据,算出来的哈希值通常是不一样的。比方说把list [1,2]存到set中,这样算出它的hash值 1001,放进了 1001 号抽屉。接着,你执行了 a.append(3),列表变成了 [1, 2, 3], 问题来了!列表的内容变了,按理说它的指纹也应该变了(假设变成了 200)。但是,它现在还躺在 set 的 1001 号抽屉里。下次你想在这个 set 里查找 [1, 2, 3] 时,Python 算出指纹是 2002,去 2002 号抽屉一看:空的!找不到了!这样整个系统就乱套了