元组与集合
你已经了解了列表。Python 还有两种集合类型,用来解决列表解决不了的问题。元组保存一组固定不变、永远不会改变的值。集合只保存唯一值,无论集合有多大,都能瞬间判断某个值是否存在其中。
元组
元组是一组创建之后就不能再改变的有序值。圆括号用来定义元组,但其实是可选的。真正让它成为元组的是逗号。只有一个元素的元组必须带一个结尾逗号。
point = (10, 20)
rgb = (255, 128, 0)
dimensions = (1920, 1080)
single = (42,) # 单元素元组必须有结尾逗号
also_tuple = 42, 99 # 圆括号是可选的;逗号才是元组的关键按索引访问的方式和列表完全一样。尝试修改某一项会抛出 TypeError:
point = (10, 20)
point[0] # 10
point[1] # 20
point[-1] # 20
point[0] = 99 # TypeError: 'tuple' object does not support item assignment(42,) 需要那个孤零零的结尾逗号。想重新赋值某一项,你会得到一个 TypeError,而这种"锁死"的感觉正是你选择元组的理由。 什么时候该用元组
当你有一小组彼此相关、不会再变化的值时,就用元组。坐标 (x, y)、颜色 (r, g, b)、姓名和分数的搭配 ("Alice", 87)。这种固定结构会向阅读代码的人传递一个信号:这组数据要当作一个整体来对待。
(r, g, b) 颜色、一对姓名和分数。因为元组可哈希,你甚至可以把它当作字典的键来用。列表做不到这一点,很多人第一次尝试时都会被这个坑到。 locations = {}
locations[(40, -74)] = "北京" # 元组作为字典键,可以正常工作
locations[[40, -74]] = "北京" # 列表作为字典键,会抛出 TypeError解包
解包能把元组里的值一次性取出来,分别赋给各自的名字,只用一行代码。名字的数量必须和值的数量一致。用 * 可以把剩下的项都收进一个列表里。
x, y = point。名字的数量必须和值的数量一致,除非你加上一个 *rest 来把剩下的都收进去。我就是从不再到处写 point[0] 和 point[1] 的那天开始,才真正弄明白这个用法的。 point = (10, 20)
x, y = point
print(x) # 10
print(y) # 20
first, *rest = [1, 2, 3, 4, 5]
# first = 1, rest = [2, 3, 4, 5]
head, *middle, tail = [1, 2, 3, 4, 5]
# head = 1, middle = [2, 3, 4], tail = 5命名元组
命名元组是一种每个位置都有名字的元组。不用再费力记住 point[0] 是 x 坐标,直接写 point.x 就行。值依然是不可变的,你只是把数字位置换成了可读的属性名。
point.x,而不用去记 point[0] 是 x。它依然是完全不可变的,除了这点,其余用法都和普通元组一样。等你以后回头看代码时,会感谢当初起了这些名字。 namedtuple 属于标准库,所以要先导入才能用:from collections import namedtuple。导入的完整讲解在模块一章。
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
Player = namedtuple("Player", ["name", "score", "level"])
p = Point(10, 20)
p.x # 10
p.y # 20
alice = Player("小美", 87, 5)
alice.name # "小美"
alice.score # 87集合
集合是一组唯一值的集合,不保证任何顺序。重复添加同一个值不会有任何效果:集合只会保留每一项的一份拷贝。用花括号加上元素来创建一个有内容的集合,或者用 set() 创建一个空集合。
set(),因为 {} 其实悄悄创建的是空字典。这最后一点几乎人人都会踩坑。 tags = {"python", "beginner", "tutorial"}
numbers = {1, 2, 3, 4, 5}
empty = set() # 不是 {}(那是空字典)重复添加同一个值不会改变集合:
tags.add("python") # tags 不变,"python" 已经在里面了什么时候该用集合
集合最适合做三件事:从列表里去除重复项,快速判断某个东西是不是在一个很大的集合中,以及比较两组数据,看它们有哪些共同点、哪些不同点。
# 从列表中去除重复项
raw = ["cat", "dog", "cat", "bird", "dog", "cat"]
unique = list(set(raw)) # ["cat", "dog", "bird"](顺序不保证)# 快速成员检测
valid_codes = {"USD", "EUR", "GBP", "JPY"}
code = "EUR"
if code in valid_codes: # 瞬间完成查找,即便有几千个代码也一样
print("有效")集合运算
集合支持你在数学课上学过的相同运算:并集(存在于任一集合中的元素)、交集(两个集合共有的元素)、以及差集(一个集合有而另一个没有的元素)。Python 用运算符符号来表示这些运算,每种运算也都有对应的方法。
| 是并集(在任一个里),& 是交集(两个都有),- 是差集(在一个里但不在另一个里),^ 是对称差集(只在一个里,不同时在两个里)。如果你更喜欢用单词而不是符号,每一种也都有对应的方法,比如 .union()。 a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a | b # {1, 2, 3, 4, 5, 6} (并集:在任一个里的都算)
a & b # {3, 4} (交集:只算两个都有的)
a - b # {1, 2} (差集:在 a 里但不在 b 里)
b - a # {5, 6} (反过来的差集)
a ^ b # {1, 2, 5, 6} (对称差集:只在一个里,不同时在两个里)这些运算也都有对应的方法形式:.union()、.intersection()、.difference()、.symmetric_difference()。
修改集合
集合是可变的。.add() 添加一个元素。.update() 从任意列表或其他可迭代对象一次性添加多个元素。.remove() 删除某个元素,但如果元素不存在会报错。.discard() 如果元素存在就悄悄删除,不存在则什么都不做。
.add() 添加一个元素,.update() 从一个列表或其他可迭代对象里一次添加一批。要分清楚的是删除操作:.remove() 找不到元素就报错,而 .discard() 会耸耸肩继续往下走。不确定元素是否在集合里时,用 .discard() 就能避免一个多余的报错。 tags = {"python", "beginner"}
tags.add("tutorial") # 添加一个元素
tags.update(["web", "api"]) # 从任何可迭代对象添加多个元素
tags.remove("beginner") # 删除元素,找不到会抛出 KeyError
tags.discard("missing") # 删除元素,找不到也不会报错
tags.pop() # 删除并返回一个任意元素
tags.clear() # 清空所有元素不确定某个元素是否存在时,用 .discard()。
冻结集合
冻结集合是一种创建之后就不能修改的集合。使用它的主要原因是:冻结集合是可哈希的,所以可以用作字典的键,或者存进另一个集合里。
valid_statuses = frozenset({"active", "paused", "deleted"})
valid_statuses.add("archived") # AttributeError,frozenset 是不可变的该选哪种集合类型
四种类型,各有明确的定位。想清楚你需要对数据做什么,答案通常就会自然浮现。
| list | tuple | set | dict | |
|---|---|---|---|---|
| 有序 | 是 | 是 | 否 | 是(按插入顺序) |
| 可变 | 是 | 否 | 是 | 是 |
| 允许重复 | 是 | 是 | 否 | 否(键) |
| 访问方式 | 索引 | 索引 | 不适用 | 键 |
| 适用场景 | 有序且可变的序列 | 固定记录 | 唯一值、快速成员检测 | 键值查找 |
一个快速的判断规则:
- 需要按名字查找? → dict
- 需要一个有序且会修改的集合? → list
- 有一组固定不变的相关值? → tuple
- 需要唯一值或快速的成员检测? → set
实践演练
用元组存储固定记录,用集合追踪唯一值:
home = (39.9042, 116.4074) # 纬度,经度
office = (39.9163, 116.3972)
home_lat, home_lon = home
print(f"家:{home_lat}, {home_lon}")
# 用集合追踪唯一访客
visitors = set()
visitors.add("小美")
visitors.add("小华")
visitors.add("小美") # 已经在集合里了,会被悄悄忽略
visitors.add("小丽")
print(f"唯一访客数:{len(visitors)}")
print(f"小美访问过:{'小美' in visitors}")
print(f"小明访问过:{'小明' in visitors}")
