跳转到内容

元组

元组(tuple)是 Python 中一种基础的数据结构。你可以把它想象成一个固定的、有顺序的容器,一旦创建好,里面的内容就不能再改变了。

元组在代码中的表现形式很简单,用圆括号 () 包裹元素,元素之间用逗号分隔:

python
t = (1, 2, 3, 4, 5)

生活中的元组

理解元组最好的方式是从生活中的例子入手:

快递单号 —— 当你寄快递时,快递公司会给你一串数字:SF1234567890。这串数字一旦生成,里面的每一位都是固定的,你不能说"把第3位改成8",否则就不是原来的单号了。元组就像这个快递单号,内容固定,顺序重要

GPS 坐标 —— 你去一个地方,经纬度是 (39.9042, 116.4074)。这个坐标有两个数字,第一个是纬度,第二个是经度,顺序不能乱,而且这个位置就是固定的。元组非常适合表示这种有固定结构的数据

身份证信息 —— 你的身份证上有姓名、性别、出生日期、地址等信息。这些信息组合在一起描述"你这个人",顺序是固定的(姓名在前,地址在后),而且一旦录入就不能随便改动。元组就像这样,用来表示一个完整事物的各个组成部分

扑克牌 —— 一张扑克牌有花色和点数,比如 ("红桃", "A")("黑桃", "10")。每张牌的这两个属性是固定的,你不会把"红桃A"改成"黑桃A",而是换一张牌。元组就是用来装这种不可拆分的、固定的组合

和列表的区别

很多人一开始分不清元组和列表(list),觉得它们差不多。关键的区别在于**"变"与"不变"**:

场景列表(list)元组(tuple)
购物清单✅ 可以添加、删除商品❌ 固定后不能改
一周七天❌ 星期几不会变("周一", "周二", ...)
考试成绩✅ 老师可以修改分数❌ 历史成绩存档后固定
RGB颜色值❌ 不会单独改R或G(255, 0, 0) 表示纯红

简单说:列表是用来"装东西"的容器,东西可以增删改;元组是用来"描述结构"的固定组合,一旦确定就不该再变。

为什么需要不可变?

你可能会问:既然列表什么都能做,为什么还要有个不能改的元组?

想象你在餐厅点了一份套餐:("汉堡", "薯条", "可乐")。这个套餐的内容是固定的,服务员不会问你要不要把汉堡换成沙拉——那就不是这个套餐了。元组的不可变性正是在表达这种"这是一个确定的组合"的语义。

此外,不可变带来两个实际好处:

  1. 可以作为字典的 key —— 就像快递单号可以作为查询包裹的索引,元组因为不变,所以能可靠地用于查找
  2. 更安全 —— 把数据传给函数时,不用担心函数会偷偷改掉你的数据

理解 tuple,不能只盯着它的不可变性。要真正理解它,需要理解它和 list 的底层差异、可哈希的真正含义、解包操作的设计哲学、以及它在 Python 语言中被广泛使用的场景。

不可变的真正含义

tuple 的核心特性是"不可变"。但这个"不可变"具体指什么?

当我说一个 tuple 是不可变的,意思是:tuple 的结构不能被修改。你不能往里面添加元素,不能删除元素,不能改变元素的顺序。

但这里有一个极其容易踩的坑:tuple 不可变的是"引用绑定",不是"递归冻结"。

也就是说,如果 tuple 里的元素本身是可变对象,那个可变对象仍然可以修改:

python
t = (1, 2, 3)
t[0] = 100  # 这会报错,tuple 结构不可变

# 但如果元素本身是可变对象
t = ([1, 2], [3, 4])
t[0].append(100)  # 这完全可以
print(t)  # ([1, 2, 100], [3, 4])

理解这个细节很重要。tuple 存储的是指向对象的引用,它保证的是"这个位置指向哪个对象"不变,而不是"那个对象本身能不能变"。

从内存模型的角度来看,tuple 和 list 的结构几乎一样,都是连续内存加指针的组合。区别在于:tuple 创建之后,这块内存就固定了,不会再扩容,也不会再缩容。这使得 tuple 比 list 更节省内存——list 需要预留额外的空间应对未来的增长,而 tuple 不需要。

单元素元组的逗号

很多人栽在单元素 tuple 的写法上:

python
a = (1)
print(type(a))  # <class 'int'>,这不是 tuple

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

关键不是括号,而是逗号。括号只是语法辅助,逗号才是真正定义 tuple 的东西。

甚至可以完全不用括号:

python
c = 1, 2, 3
print(type(c))  # <class 'tuple'>
d = 1,
print(type(d))  # <class 'tuple'>

这个细节在面试中经常出现。不写那个逗号,括号包着的只是一个普通的值,不是 tuple。

可哈希与字典的 key

tuple 最重要的用途之一是作为字典的 key。这是因为 tuple 是可哈希的。

可哈希是什么意思?简单来说,一个对象如果可以被哈希,那么它在生命周期内应该返回一个恒定的整数哈希值。这个哈希值被用来快速定位字典中的槽位。

为什么可变对象不能哈希?

想象一下,如果 list 可以作为字典的 key:

python
key = [1, 2]
d = {key: "value"}

当你修改 key 的时候:

python
key.append(3)

这个 key 的哈希值就变了。但字典仍然用原来的哈希值去查找,找到的却是一个完全不同的槽位。这就是哈希表的逻辑矛盾——一个东西改变了,但字典不知道它改变了,仍然用旧的信息去找它。

所以 Python 规定:只有不可变对象才能作为字典的 key。tuple 不可变,所以可以被哈希。

但这里有一个例外需要注意:

python
t = ([1, 2], 3)
hash(t)  # 这会报错!

因为这个 tuple 包含了一个可变对象(list),所以整体上不可哈希。规则是:只有当 tuple 中的所有元素都是可哈希的,这个 tuple 才是可哈希的

面试时经常问这个问题。正确的回答是:tuple 的可哈希性取决于其内部元素——如果所有元素都是不可变对象(int、str、tuple 等),这个 tuple 就是可哈希的。

解包:Python 最优雅的设计之一

tuple 的解包(unpacking)是 Python 中最优雅的特性之一。它的本质是"模式匹配"。

当你写:

python
x, y = 1, 2

Python 实际上是在做:创建一个 tuple (1, 2),然后把这个 tuple 的元素依次赋值给 x 和 y。这个过程是 Python 语法层面支持的,比很多语言需要临时变量然后赋值要简洁得多。

函数返回多个值时,其实返回的也是 tuple:

python
def get_point():
    return 1, 2

result = get_point()
print(type(result))  # <class 'tuple'>

这个设计哲学是:函数返回多个值,本质上就是返回一个有序的聚合体,tuple 是最自然的选择。

解包还可以用来交换两个变量:

python
a, b = 1, 2
a, b = b, a  # 不需要临时变量

Python 内部会创建一个临时的 tuple (b, a),然后解包赋值给 (a, b)。这种写法比很多语言需要临时变量要优雅得多。

还有一种高级用法是星号解包:

python
first, *rest, last = [1, 2, 3, 4, 5]
print(first)  # 1
print(rest)   # [2, 3, 4]
print(last)   # 5

星号解包会把剩余的元素收集成一个列表。这个特性在处理变长参数时非常有用。

tuple 与函数参数

Python 的函数参数机制底层也用到了 tuple。

当你定义:

python
def func(*args):
    print(type(args))

args 就是一个 tuple。这不是巧合,而是设计选择。

为什么用 tuple 而不是 list?因为 tuple 更轻量,而且语义上"参数数量固定后不需要再修改",tuple 的不可变性正好符合这个语义。

同样的,**kwargs 底层用的是 dict。

python
def func(**kwargs):
    print(type(kwargs))

理解这个底层机制有助于理解 Python 函数调用时参数传递的完整图景。

命名元组与数据结构

namedtuple 是 tuple 的一个扩展,它在保留 tuple 不可变特性和内存效率的同时,添加了通过名字访问字段的能力。

python
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(3, 5)

print(p.x)      # 3,可以像属性一样访问
print(p[0])     # 3,也可以用索引访问
print(p.x == p[0])  # True,两者等价

namedtuple 的底层实现很巧妙:它继承自 tuple,所以内存布局和 tuple 完全一样,没有额外的实例字典。同时它通过 __getattr__ 拦截属性访问,把字段名映射到对应的索引上。

namedtuple 和普通 class 的关键区别在于:namedtuple 是不可变的,而且因为没有 __dict__,它的内存占用远小于普通 class。在需要创建大量简单数据对象的场景下,这是很重要的性能优化。

tuple 的性能优势

因为 tuple 的大小固定,不需要动态扩容,所以 tuple 在很多方面比 list 更高效。

首先是内存占用。由于不需要预留空间用于未来的增长,tuple 的内存分配更紧凑:

python
import sys

print(sys.getsizeof([1, 2, 3]))  # list 更大
print(sys.getsizeof((1, 2, 3)))  # tuple 更小

其次是创建速度。tuple 的创建只需要一次性分配内存,而 list 需要考虑扩容策略。

在访问性能上,两者没有区别——索引访问都是 O(1) 的连续内存访问。

python
t = (1, 2, 3)
l = [1, 2, 3]

print(t[1])  # O(1)
print(l[1])  # O(1),两者一样快

什么时候用 tuple

既然 list 什么都能干,为什么还要用 tuple?

答案在于语义。当你使用 tuple 时,你在告诉未来的读者和编译器:这个数据是"结构",不是"集合"。它的意义在于"由哪些部分组成",而不是"包含哪些元素"。

典型的使用场景包括:

函数的返回值。如果函数返回的是一个有固定结构的数据(比如坐标、颜色、日期),用 tuple 比用 list 更清晰,也更能防止被意外修改。

字典的 key。如果你需要用复合数据作为字典的 key,必须使用 tuple。

作为集合的元素。如果你想让集合包含不可变的复合数据,tuple 是唯一选择。

配置常量。把不会改变的数据用 tuple 存储,可以防止意外的修改。

python
# 固定结构用 tuple
point = (0, 0)
color = (255, 0, 0)

# 动态数据用 list
scores = [85, 90, 78]

常见误区

第一个误区是认为 tuple 完全不可变。虽然 tuple 的结构不可变,但如果包含可变对象,那个可变对象仍然可以修改。如果需要递归不可变,应该使用冻结集合或者其他自定义结构。

第二个误区是忘记单元素 tuple 后面要加逗号。这是 Python 初学者最常犯的错误之一。

第三个误区是混用 tuple 和 list。如果你明确知道数据不会改变,用 tuple 更合适,它不仅更安全,也更高效。如果你需要频繁修改,用 list。

第四个误区是在需要可哈希时使用了包含可变对象的 tuple。记住,只有所有元素都可哈希时,tuple 才是可哈希的。

list 和 tuple 的对比

理解两者的区别是 Python 面试的基础问题:

特性listtuple
可变性可变不可变
内存占用较大(需要预留空间)较小(固定大小)
创建速度较慢(考虑扩容)较快(一次性分配)
可作为 dict key条件允许时可行
可哈希元素全为可哈希对象时可行
适用场景动态数据固定结构

本质上,list 和 tuple 的区别不只是"能不能改",而是"设计哲学不同"。list 是为了"容器",tuple 是为了"结构"。

基于 MIT 许可发布