跳到主要内容

一个任意的周期函数都可以表示为简单正弦波的和。

Joseph Fourier傅里叶分析创始人

📦 元组(Tuple)

元组(tuple)是 Python 中有序、不可变的序列。它和列表很像,但一旦创建就不能修改——这种"只读"特性让它在表达固定数据(如坐标、配置项、记录)时更安全、更高效。元组也是函数多返回值的底层载体。

📌 本节要点

  • 元组是不可变序列,创建后不能增删改元素
  • 解包赋值:x, y = point 一次取出多个值
  • 命名元组 NamedTuple 为元组字段赋予名称,兼具可读性和性能
  • 元组可以作为字典键和集合元素(列表不行)
  • 单元素元组需要尾逗号:(1,) 而非 (1)
  • 元组拆包 * 运算符:first, *rest = seq
元组操作快速体验

创建元组

Python
# 1. 圆括号字面量
point = (3, 4)
rgb = (255, 128, 0)

# 2. 不带括号的"隐式元组"
coordinates = 3, 4
print(coordinates) # (3, 4)
print(type(coordinates)) # <class 'tuple'>

# 3. tuple() 构造器
from_tuple = tuple([1, 2, 3])
from_string = tuple("abc")
print(from_tuple) # (1, 2, 3)
print(from_string) # ('a', 'b', 'c')

# 4. 空元组
empty: tuple = ()
also_empty = tuple()
圆括号可省略

x, y = 3, 4 中右侧的 3, 4 其实就是一个元组。圆括号常用于提升可读性或在函数调用中消歧义,但语法上经常可省。

不可变性

元组的元素不可重新赋值,也不能增删。

Python
point = (3, 4)
# point[0] = 5 # TypeError: 'tuple' object does not support item assignment

# 但可以创建新元组
new_point = (5, 4)
point = point + (5,) # 拼接生成新元组
print(point) # (3, 4, 5)
不可变是"浅层"的

元组本身的元素引用不能改,但如果元素本身是可变对象,仍可修改该对象内部。

Python
data = (1, [2, 3], 4)
# data[1] = [9, 9] # TypeError 不可改引用
data[1].append(99) # OK,改的是内层列表
print(data) # (1, [2, 3, 99], 4)

单元素元组陷阱

⚠️ 单元素元组陷阱

这是元组最容易踩的坑:单元素元组必须带尾随逗号。

Python
# 正确:尾随逗号
single = (42,)
print(type(single)) # <class 'tuple'>

# 错误:这只是带括号的表达式,不是元组
not_tuple = (42)
print(type(not_tuple)) # <class 'int'>

# 隐式写法同样需要逗号
also_single = 42,
print(type(also_single)) # <class 'tuple'>
为什么尾随逗号

Python 语法中 (42) 只是普通表达式(如计算 (2 + 3) * 4),括号仅用于分组。要表示元组必须靠逗号 (42,)。空元组 ( ) 是唯一例外,不需要逗号。

:::

解包

元组解包(unpacking)是把序列中的元素一次性赋给多个变量的优雅写法,是 Python 风格的标志性特性。

Python
point = (3, 4)

# 基础解包
x, y = point
print(x, y) # 3 4

# 交换变量(无需临时变量)
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1

# 用 * 收集多余元素
first, *rest = (1, 2, 3, 4, 5)
print(first, rest) # 1 [2, 3, 4, 5]

*init, last = (1, 2, 3, 4, 5)
print(init, last) # [1, 2, 3, 4] 5

first, *middle, last = (1, 2, 3, 4, 5)
print(first, middle, last) # 1 [2, 3, 4] 5
函数返回值解包

多返回值本质就是返回一个元组,再被解包。

Python
def min_max(numbers: list[int]) -> tuple[int, int]:
return min(numbers), max(numbers)

low, high = min_max([3, 1, 4, 1, 5, 9, 2, 6])
print(low, high) # 1 9
Python 3.12 更新

Python 3.12 引入了更灵活的解包语法预览,未来版本将进一步放宽对 * 解包在表达式中的限制。当前 3.12+ 已稳定支持上述所有用法。

命名元组

普通元组靠位置访问,可读性差。collections.namedtupletyping.NamedTuple 让元组既能按名字访问字段,又保持元组的不可变与轻量。

collections.namedtuple

Python
from collections import namedtuple

# 创建类型
Point = namedtuple("Point", ["x", "y"])

p = Point(3, 4)
print(p.x, p.y) # 3 4 按字段名访问
print(p[0], p[1]) # 3 4 仍可按索引访问
print(p) # Point(x=3, y=4)

# 它是 tuple 的子类,可以用在需要元组的地方
print(isinstance(p, tuple)) # True

# 支持 _replace 生成新实例(不改原对象)
p2 = p._replace(x=10)
print(p, p2) # Point(x=3, y=4) Point(x=10, y=4)

# 转换为字典
print(p._asdict()) # {'x': 3, 'y': 4}

typing.NamedTuple

typing.NamedTuple 支持类型注解,更现代、更推荐。

Python
from typing import NamedTuple


class Point(NamedTuple):
x: float
y: float
label: str = "origin"


p = Point(3.0, 4.0, "A")
print(p.x, p.y, p.label) # 3.0 4.0 A

# 同样支持 _replace 和解包
x, y, label = p
print(x, y, label) # 3.0 4.0 A

p_moved = p._replace(x=5.0)
print(p_moved) # Point(x=5.0, y=4.0, label='A')
选择哪一个

新代码优先用 typing.NamedTuple,它带类型注解、与 IDE/类型检查器配合更好。collections.namedtuple 适合需要动态生成字段名的场景。

元组 vs 列表

特性列表 list元组 tuple
可变性可变不可变
语法[]()a, b
方法多(append/pop/sort...)少(count/index)
性能略慢、占内存稍多更快、更省内存
可哈希元素均可哈希时,是
用途动态数据集合固定数据记录、多返回值
Python
import sys

lst = [1, 2, 3, 4, 5]
tup = (1, 2, 3, 4, 5)
print(sys.getsizeof(lst)) # 列表占用更大
print(sys.getsizeof(tup)) # 元组占用更小
不可变的好处

不可变性带来三点收益:①防止误改数据;②可作为字典键与集合元素(可哈希);③解释器可以做更多优化,访问更快。

元组作为字典键

因为元组不可变,所以当其元素也都可哈希时,元组本身也可哈希,能作为字典的键。这是元组相比列表的核心优势之一。

Python
# 用 (城市, 日期) 作为键存储天气数据
weather: dict[tuple[str, str], float] = {
("北京", "2025-01-01"): -3.2,
("上海", "2025-01-01"): 5.1,
("北京", "2025-01-02"): -1.8,
}

# 查询
key = ("北京", "2025-01-01")
print(weather.get(key)) # -3.2

# 列表不能作为键
data = {}
# data[[1, 2]] = "x" # TypeError: unhashable type: 'list'
data[(1, 2)] = "x" # OK
注意嵌套可变对象

如果元组里包含列表,整个元组就不可哈希,不能作为字典键。

Python
# bad = {(1, [2, 3]): "x"} # TypeError: unhashable type: 'list'
good = {(1, (2, 3)): "x"} # OK 内层也是不可变

实战:坐标点

下面用 typing.NamedTuple 实现一个二维坐标点,并演示距离计算与不可变性带来的安全。

Python
from __future__ import annotations

import math
from typing import NamedTuple


class Point(NamedTuple):
x: float
y: float

def distance_to(self, other: Point) -> float:
"""计算到另一个点的欧几里得距离。"""
return math.hypot(self.x - other.x, self.y - other.y)

def translated(self, dx: float, dy: float) -> Point:
"""返回平移后的新点(不修改原点)。"""
return Point(self.x + dx, self.y + dy)


def main() -> None:
origin = Point(0.0, 0.0)
p = Point(3.0, 4.0)

print(f"原点: {origin}")
print(f"到原点距离: {p.distance_to(origin)}") # 5.0

# 不可变:所有"修改"都返回新对象
moved = p.translated(1.0, 1.0)
print(f"平移后: {moved}") # Point(x=4.0, y=5.0)
print(f"原点不变: {p}") # Point(x=3.0, y=4.0)

# 可作为字典键
grid: dict[Point, str] = {
Point(0, 0): "原点",
Point(1, 0): "右",
Point(0, 1): "上",
}
print(grid[Point(0, 1)]) # 上

# 解包
x, y = p
print(f"解包: x={x}, y={y}") # 解包: x=3.0, y=4.0


if __name__ == "__main__":
main()
不可变 + 方法

这个例子体现了"函数式风格":所有变换都返回新对象,原对象永不被修改,便于追踪与并发。这是元组/NamedTuple 的精髓用法。

🎯 动手练习

  1. 单元素元组:创建包含单个整数、单个字符串、单个列表的元组,验证类型
  2. 变量交换:使用元组解包交换三个变量 a, b, c = 1, 2, 3 的值(循环移位)
  3. NamedTuple 实践:定义一个 Student(name, age, grade) NamedTuple,并演示 _replace 方法
  4. 字典键实验:尝试用列表、元组、包含列表的元组分别作为字典键,观察哪些成功

📚 延伸阅读

📋速查表
创建元组`(a, b)` 或 `a, b`
point = 3, 4
空元组`()` 或 `tuple()`
empty = ()
单元素元组`(x,)`
single = (42,)
索引访问`t[i]`
t[0]
解包`a, b = t`
x, y = (3, 4)
星号解包`first, *rest = t`
1, [2, 3, 4]
拼接`t1 + t2`
(1, 2) + (3,) → (1, 2, 3)
重复`t * n`
(1, 2) * 3 → (1, 2, 1, 2, 1, 2)
命名元组`typing.NamedTuple`
class Point(NamedTuple): x: int; y: int
替换`t._replace(x=val)`
p._replace(x=10)

✅ 本节总结

  • 元组是有序、不可变的序列,用 () 或隐式逗号创建
  • 单元素元组必须带尾随逗号 (x,),这是最常见的坑
  • 不可变是浅层的:元素引用不能改,但可变元素内部仍可改
  • 解包(含 * 收集)是 Python 风格的标志性特性,函数多返回值即基于此
  • typing.NamedTuple 提供带类型注解的命名元组,是新代码首选
  • 可哈希的元组可作字典键与集合元素,这是相对列表的核心优势
  • 表达"固定记录"用元组,表达"动态集合"用列表