元组
元组(tuple)是 Python 的核心序列类型之一,与列表共享索引、切片、迭代等操作,但具有不可变性(immutable)。这种不可变性使元组在特定场景下比列表更安全、更高效,也使其能够作为字典的键或集合的元素。
创建元组
元组由逗号分隔的值组成,圆括号在大多数情况下只是可选的分组符号:
t = 12345, 54321, 'hello!' # 合法,圆括号可省略
print(t) # (12345, 54321, 'hello!')
print(t[0]) # 12345
当元组作为更大表达式的一部分时,圆括号变得必要,否则逗号会被解析为参数或运算符分隔符:
# 作为函数参数的一部分
len((1, 2, 3)) # 圆括号区分元组与多个参数
# 元组嵌套
u = t, (1, 2, 3, 4, 5)
print(u) # ((12345, 54321, 'hello!'), (1, 2, 3, 4, 5))
空元组用一对空圆括号创建:empty = ()。单元素元组是创建时的常见陷阱——必须在元素后加逗号,否则圆括号会被当作分组运算符:
singleton = 'hello', # ✅ 元组,注意末尾逗号
print(len(singleton)) # 1
print(singleton) # ('hello',)
not_tuple = ('hello') # ❌ 只是字符串 'hello'
print(type(not_tuple)) # <class 'str'>
末尾逗号在输出时也会显示,这是 Python 区分单元素元组与分组表达式的语法标记。
不可变性
元组一旦创建,就不能通过索引赋值、切片赋值或任何方法修改其结构:
t = (1, 2, 3)
t[0] = 99 # TypeError: 'tuple' object does not support item assignment
del t[0] # TypeError: 'tuple' object doesn't support item deletion
但不可变性只作用于元组容器的结构,不限制其内部可变对象的内容。如果元组包含列表、字典等可变对象,这些对象的内容仍可修改:
v = ([1, 2, 3], [3, 2, 1])
v[0][0] = 99
print(v) # ([99, 2, 3], [3, 2, 1])
这种"浅不可变"特性意味着,包含可变对象的元组不能安全地作为字典键或集合元素,因为其等效性可能在创建后改变,破坏哈希容器的内部一致性。
元组与列表的使用场景有明确分野:列表适合同质元素的动态集合,需要频繁增删;元组适合异质数据的固定记录,作为不可变标识或轻量结构体使用。
元组解包
元组支持序列解包(sequence unpacking),将右侧元组的元素依次赋给左侧变量:
t = (12345, 54321, 'hello!')
x, y, z = t
print(x) # 12345
print(y) # 54321
print(z) # hello!
解包要求左侧变量数与右侧元素数严格匹配,否则会报错:
a, b = (1, 2, 3) # ValueError: too many values to unpack
a, b, c, d = (1, 2) # ValueError: not enough values to unpack
扩展解包使用星号 * 收集剩余元素,解决数量不匹配的问题:
first, *rest = (1, 2, 3, 4)
print(first) # 1
print(rest) # [2, 3, 4],注意结果是列表
*rest, last = (1, 2, 3, 4)
print(rest) # [1, 2, 3]
print(last) # 4
first, *middle, last = (1, 2, 3, 4, 5)
print(middle) # [2, 3, 4]
星号变量收集的结果永远是列表,即使只有一个元素或没有元素:
a, *b = (1,) # a=1, b=[]
a, *b, c = (1, 2) # a=1, b=[], c=2
嵌套元组也支持嵌套解包:
data = ("Alice", (25, "Engineer"))
name, (age, job) = data
print(age) # 25
print(job) # Engineer
命名元组
标准库 collections 模块提供的 namedtuple() 工厂函数,可以创建带字段名的元组子类。命名元组兼具元组的不可变性和类的可读性:
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(11, y=22)
print(p.x) # 11,字段名访问
print(p[0]) # 11,索引访问依然有效
print(p.y) # 22
命名元组与普通元组完全兼容,支持解包、索引、迭代,同时提供自文档化的字段名。对于简单数据记录,命名元组比完整类定义更轻量:
Employee = namedtuple('Employee', 'name dept salary')
emp = Employee('Alice', 'Engineering', 80000)
# 解包
name, dept, salary = emp
# 转换为字典
emp_dict = emp._asdict()
# {'name': 'Alice', 'dept': 'Engineering', 'salary': 80000}
# 替换字段创建新实例(元组不可变,返回新对象)
emp2 = emp._replace(salary=90000)
Python 3.7+ 中,如果不需要与旧版本兼容,数据类(@dataclass)提供了更强大的替代方案;但在需要不可变性、哈希性或最小内存开销的场景下,命名元组仍是优秀选择。