元组和集合:Python 里最被低估的两个数据结构

新人学 Python,上来就是列表字典,元组和集合基本跳过。

说实话,这俩东西你用好了,代码能少写不少 bug,性能还能提一截。

元组:不可变的列表

元组用圆括号:

point = (3, 4)
person = ("张三", 28, "北京")
empty = ()
single = (1,)   # 单个元素的元组,逗号不能省!

和列表最大的区别:元组创建后不能修改

nums = (1, 2, 3)
# nums[0] = 99  # TypeError! 元组不能改
# nums.append(4) # AttributeError! 没有 append

那既然有列表了,为什么还要元组?

原因1:保护数据不被意外修改

# 这个函数返回的坐标就不该被调用方改
def get_mouse_position():
    return (150, 300)  # 元组

# 用列表的话,外面不小心 append 一下,数据就脏了

原因2:元组可以当字典的键

# 用元组存坐标 → 字典键
locations = {
    (39.9, 116.4): "北京",
    (31.2, 121.5): "上海",
    (22.5, 114.1): "深圳",
}
print(locations[(31.2, 121.5)])  # 上海

列表是可变的,不能当键。元组不可变,可以。

原因3:函数返回多个值用的就是元组

def get_user():
    return "张三", 28, "北京"  # 其实返回的是元组

name, age, city = get_user()  # 解包
print(type(get_user()))       # <class 'tuple'>

原因4:元组比列表快一点、省一点内存

数据量大的时候能感觉到差异。不过对新手来说前面三条更重要。

元组的操作
t = (1, 2, 3, 2, 4)

print(len(t))        # 5
print(t[0])          # 1
print(t[1:3])        # (2, 3)
print(t.count(2))    # 2 — 统计出现次数
print(t.index(3))    # 2 — 查找位置
print(3 in t)        # True
print(max(t))        # 4
print(sum(t))        # 12

# 解包
a, b, c = (1, 2, 3)
a, *rest, c = (1, 2, 3, 4, 5)  # a=1, rest=[2,3,4], c=5

集合:自动去重 + 数学运算

集合用花括号(但空花括号是字典,空集合用 set()):

fruits = {"苹果", "香蕉", "橘子"}
empty = set()  # 空集合,{} 是空字典

集合三大特点:

  1. 元素不重复(自动去重)
  2. 元素无序(没有索引)
  3. 元素必须可哈希(不可变类型,如数字、字符串、元组)
去重:一行搞定
nums = [1, 2, 2, 3, 3, 3, 4, 5, 5]
unique = list(set(nums))
print(unique)  # [1, 2, 3, 4, 5]  顺序可能不同

比手写循环判重优雅太多了。

增删查
s = {1, 2, 3}

s.add(4)          # 加一个
s.add(2)          # 重复的不会加进去
s.update([5, 6])  # 批量加
s.remove(3)       # 删除,不存在会报错
s.discard(10)     # 删除,不存在也不报错
s.pop()           # 随机删一个(因为无序)
print(3 in s)     # 查 — 集合的查找是 O(1),比列表的 O(n) 快
数学运算:集合的灵魂
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a | b)   # {1,2,3,4,5,6} — 并集
print(a & b)   # {3,4} — 交集
print(a - b)   # {1,2} — 差集(a有b没有)
print(b - a)   # {5,6} — 差集(b有a没有)
print(a ^ b)   # {1,2,5,6} — 对称差集(只在一边的)

这玩意儿在做"两个列表的交集/差集"时特别好用:

yesterday_visitors = {"张三", "李四", "王五"}
today_visitors = {"李四", "王五", "赵六"}

new_users = today_visitors - yesterday_visitors  # {"赵六"}
returning = yesterday_visitors & today_visitors  # {"李四", "王五"}
lost = yesterday_visitors - today_visitors       # {"张三"}

什么时候用什么?

场景 选哪个 原因
需要增删改 列表 可变
数据不该被改 元组 不可变,安全
字典的键 元组 不可变
要去重 集合 自动去重
要快速查找 集合 O(1)
两个序列的交差并 集合 数学运算
保持顺序 列表/元组 集合无序

一句话总结:列表是主力,元组保安全,集合做去重和运算。

新手常见坑

坑1:单个元素元组忘加逗号

t = (1)     # 这不是元组,是整数 1(括号被当成运算优先级)
print(type(t))  # <class 'int'>

t = (1,)    # 这才是元组
print(type(t))  # <class 'tuple'>

坑2:集合放不了可变元素

# s = {[1, 2], [3, 4]}  # TypeError! 列表不可哈希
s = {(1, 2), (3, 4)}   # 用元组就行

坑3:空集合用 {} 是字典

d = {}
print(type(d))  # <class 'dict'>

s = set()
print(type(s))  # <class 'set'>

记住:空集合 = set(),空字典 = {}

动手试试

  1. 用集合去重:给一句话,输出不重复的字符
  2. 求出两个班级名单的公共学生和独有学生
  3. 用元组存储多个坐标点,计算离原点最近的点

参考答案:

# 1. 去重
sentence = "hello world"
unique_chars = set(sentence)
print(unique_chars)

# 2. 班级名单
class_a = {"张三", "李四", "王五", "赵六"}
class_b = {"李四", "赵六", "孙七", "周八"}
print("公共:", class_a & class_b)
print("A有B没有:", class_a - class_b)

# 3. 最近点
import math
points = [(1, 2), (3, 4), (0, 1), (5, 0)]
nearest = min(points, key=lambda p: math.sqrt(p[0]**2 + p[1]**2))
print(nearest)  # (0, 1)

写在最后

元组和集合是两个"平时不起眼,用上了真香"的数据结构。尤其是集合的去重和数学运算,很多新手用列表+循环硬写几十行的事,集合一行就能搞定。

下一篇聊条件判断——ifelifelse,让你的代码开始做决策。