2. 常见序列类型:字符串、列表、元组的对比与适用场景

序列类型,说白了就是一组有顺序的数据容器。Python 里最常见的三种——字符串、列表、元组——我几乎每天都在跟它们打交道。它们都能用下标访问,都能切片,都能迭代,但各自的脾气秉性完全不同。

我个人习惯把这三兄弟放在一起对比着学,因为只有搞清楚它们「哪里像、哪里不像」,写代码时才能选对工具。你想想看,选错了类型,轻则代码别扭,重则埋下性能或安全方面的坑。

2.1 三者的核心差异

先看一张对比表,把最关键的几个维度列清楚:

特性 字符串 (str) 列表 (list) 元组 (tuple)
可变性 不可变 可变 不可变
元素类型 只能是字符 任意类型 任意类型
表示方式 'hello' 或 "hello" [1, 2, 3] (1, 2, 3)
性能(访问/遍历) 快 较快 最快
哈希能力 可哈希(可用作字典键) 不可哈希 可哈希(元素均为可哈希时)
典型用途 文本处理 动态数据集合 固定结构、函数返回值

嗯,这里有个关键点:可变性是区分它们最重要的标尺。列表可以增删改,字符串和元组一旦创建就不能变了。这个特性直接决定了你能对它们做什么操作,以及它们能用在哪些场景。

核心原则:需要频繁修改数据 → 选列表;数据一旦确定不再改变 → 选元组;处理文本 → 选字符串。别把元组当列表用,也别把列表塞进字典当键。

2.2 字符串:文本处理的基石

字符串没什么好说的,就是文本。但我在项目中见过太多人把它当列表用,比如用 str.replace() 循环替换字符,结果性能惨不忍睹。

为什么?因为字符串不可变。每次「修改」其实都创建了一个新字符串对象。你循环 1000 次,就创建了 1000 个临时对象。

# 错误示范:循环拼接字符串
s = ""
for i in range(10000):
    s += str(i)  # 每次创建新字符串,O(n²)

# 正确做法:用列表收集,最后 join
parts = []
for i in range(10000):
    parts.append(str(i))
s = "".join(parts)  # 一次拼接,O(n)

我曾经在一个日志处理脚本里踩过这个坑,处理 10 万行日志跑了 3 分钟。改成 join 之后,3 秒搞定。嗯,有时候性能问题就藏在这些细节里。

2.3 列表:最灵活的序列

列表是我用得最多的序列类型。它可变、可嵌套、可放任何东西。说白了就是一个「万能容器」。

但灵活也意味着责任。列表作为可变对象,有个容易踩的坑:作为函数默认参数。

def add_item(item, target_list=[]):
    target_list.append(item)
    return target_list

print(add_item(1))  # [1]
print(add_item(2))  # [1, 2]  ← 同一个列表对象!
print(add_item(3))  # [1, 2, 3]

我刚开始学 Python 时就被这个坑过。默认列表只创建一次,后续调用都在修改同一个对象。避坑指南:默认参数用 None,函数内部再创建新列表。

def add_item(item, target_list=None):
    if target_list is None:
        target_list = []
    target_list.append(item)
    return target_list

2.4 元组:不可变的「记录」

元组在 Python 里经常被低估。很多人觉得它就是「不能改的列表」,其实它的真正价值在于表达固定结构。

举个例子,函数返回多个值时,本质上返回的就是一个元组:

def get_user_info(user_id):
    # 模拟查询
    return ("张三", 28, "工程师")  # 实际上返回的是元组

name, age, title = get_user_info(1001)
print(name, age, title)  # 张三 28 工程师

我个人习惯用元组表示「记录」——比如坐标 (x, y)、RGB 颜色 (255, 128, 0)、数据库里的一行数据。因为这些数据天然就是固定字段、固定顺序,不该被修改。

小技巧:元组可以作为字典的键,列表不行。如果你需要把一组固定值作为键来查表,用元组。比如 cache = {("GET", "/api/users"): response_data}。

2.5 适用场景速查

下面这张表是我自己总结的,平时写代码时经常对照着看:

场景 推荐类型 原因
文本处理、正则匹配 字符串 丰富的字符串方法,不可变保证安全
动态数据收集(如用户输入) 列表 需要频繁 append / pop / 修改
函数返回多个值 元组 解包方便,不可变防止误改
作为字典键或集合元素 元组 / 字符串 可哈希,列表不行
配置常量、枚举值 元组 不可变,语义清晰
需要排序、去重、随机访问 列表 内置 sort / set 转换 / 下标访问
大量数据只读遍历 元组 内存占用更小,访问更快

2.6 知识结构图

下面这张 SVG 图把三种序列类型的关系、特性和转换路径画清楚了:

三种序列类型对比与转换 三种序列类型对比与转换 字符串 (str) 不可变 · 仅字符 列表 (list) 可变 · 任意类型 元组 (tuple) 不可变 · 任意类型 核心特性对比 不可变 可变 不可变 可哈希 不可哈希 可哈希(条件) 常用转换路径 str() → 字符串 | list() → 列表 | tuple() → 元组 | ''.join() → 列表→字符串 选择建议 文本 → 字符串 | 动态数据 → 列表 | 固定记录 → 元组 需要哈希 → 字符串/元组 | 需要修改 → 列表 | 追求性能 → 元组

2.7 避坑总结

最后,把我这些年踩过的坑汇总一下,你写代码时留个心眼:

  • 别用 + 或 += 循环拼接字符串 —— 用列表收集 + join(),性能差距可能是数量级的。
  • 别把可变对象(如列表)作为函数默认参数 —— 用 None 加内部初始化。
  • 别试图修改元组 —— 它不可变。真想改,转成列表再改,或者新建元组。
  • 别把列表当字典键 —— 它不可哈希。用元组代替。
  • 别在遍历列表时增删元素 —— 遍历副本或改用列表推导式。

我曾经在生产环境里遇到过一个诡异的 bug:一个函数用列表做默认参数,结果两次调用之间状态互相污染,查了整整一个下午。从那以后,我对可变默认参数有了心理阴影。嗯,希望你不用经历这种痛苦。

三种序列类型,各有各的脾气。选对了,代码干净利落;选错了,处处是坑。记住一句话:字符串管文本,列表管动态,元组管固定。大部分场景下,这个原则够用了。