跳转至

字符串、dict、tuple — 三种容器的分工

这一章讲三种数据结构,但只为一个问题服务:一个东西该放进哪种容器? 原书的答案很硬:字符串只存文本;dict 管「键 → 值」的查找;tuple 是「几个值的只读组合」。 判断标准不是「哪个功能多」,而是「这个东西在问题里扮演什么角色」。

1. 这一章讲什么

上一章(list)解决「一组同类对象」;本章解决剩下的三种日常容器,原书各给了一句话的定位:

  • 字符串(text):代码里的文本模块,两头带引号;1
  • dict(字典):按名字存值,运行时可以动态地加条目——普通变量写代码时就定死了名字,dict 的键可以到时候才有;2
  • tuple(元组):「几个值的组合」,和列表几乎一样,但不能改。3
  容器        编制              什么时候用
  str         一段文本          只有真的是文本时(人读的话)
  dict        名字 → 值         键名运行时才确定;按名字查,不按位置
  tuple       只读的组合         函数一次吐回好几个值;不该再拆开重组
  图说:三种容器 + 上一章的 list,构成 Python 日常数据的四种编制。

2. 字符串:只存文本,以及 f-string 这个主力技能

2.1 创建与拼接

解决什么问题: 程序要处理人读的文本:名字、句子、文件内容。

怎么做: 单双引号随意("House" 和 'House' 等价;原书习惯双引号,REPL 显示时偏好单引号)。 两个基础操作:加号拼接、split 按分隔符(比如逗号、空格)拆开:

"Hello " + "World"          # 'Hello World'
"1,2,3,4".split(",")        # ['1', '2', '3', '4'] —— 读 CSV 的基本动作
"Hello World".split(" ")    # ['Hello', 'World']

顺带一提:"Hello " * 3 是 'Hello Hello Hello '——字符串可以乘,书里评价「好玩,但没什么用」。4

字符串 ≈ 只读列表(第 04 章的桥):能遍历 for letter in "Hello"、能按下标取,但改单个字符会报 TypeError。 所以列表那章的遍历直觉直接平移过来。5

2.2 f-string:把变量的值织进文本

解决什么问题: 生成「含变量内容」的句子,比如 I have 10 Apple in stock.,其中数量和品名来自变量。

怎么做(主线例子): 字符串开头加 f,要插入的变量名用大括号包起来:6

fruit_name = "Apple"
quantity = 10
f"I have {quantity} {fruit_name} in stock."   # 'I have 10 Apple in stock.'

走一遍:f 告诉 Python「这个串里有大括号,大括号里的是变量名,把它现在的值织进来」; 数量和名字被替换成 10 和 Apple,其余字面原样保留。数字、列表都行——「Python 里任何对象都能转成字符串」,所以任何对象都能进 f-string。7

为什么有效、边界在哪: 它是 Python 3.8 引入的写法,也是今天写 Python 的默认姿势 (更早的 % 格式化和 .format() 照旧能跑,但新代码不用学它们)。 边界:大括号里的表达式别写复杂——f"{...}" 里塞运算,可读性立刻塌方;复杂的先算好再织。

2.3 特殊字符与三种变体字符串

解决什么问题: 换行、引号、反斜杠这类字符有「代码含义」,直接写进字符串会被解释器抢去当指令 (书里把它类比成 SQL 注入:特殊字符混进字符串、被当成代码执行——凡是读用户输入,都要确保内容只被当字符串)。 最简单的绕行:串里要用单引号就用双引号包,反之亦然。8

三种变体,各管一种「普通字符串不舒服」的场景:9

变体 写法 管什么
f-string f"...{name}..." 织入变量值
多行字符串 """..."""(三引号) 跨多行的文本
原始字符串 r"Here is a \n" 反斜杠不转义,\n 就显示成两个字符

2.4 头号反模式:拿字符串存结构化信息

书里把这个警告放进了加框的「Using of Strings」:

字符串只该用来存文本,别的不存。 常见反模式是拿字符串存某种「结构化对象」式的信息——非常容易出错。 要存结构化信息,用类(第 11 章);存简单选项,用 enum(第 06 章);存数据文件,用 XML/JSON 这类为结构化数据设计的格式。10

第 03 章那句「别碰字符串,除非万不得已」在这里落地了:字符串没有结构,解析字符串全靠约定,而约定会 silent 地崩。 作者给的收尾规矩同样硬:「字符串要在读进来的当时就处理好;其余代码里不该有任何字符串逻辑。永远不。」 (例:读 CSV 时 "42" 当场 int() 成 42,之后的世界里没有字符串。)

一个顺带的小勘误:书里示例用了 "Hello".contains("l")——contains 不是 Python 字符串的方法, 正确写法是 "l" in "Hello"(返回 True)。照书敲会报 AttributeError。4

3. dict:按名字存取,运行时动态生长

3.1 创建、存取、遍历

解决什么问题: 普通变量的名字在写代码时就定死;如果键名要到运行时才知道(比如读进来的文件), 就需要一个「名字也能动态」的容器。

怎么做(主线例子): 一本菜谱,菜名 → 原料清单:2

recipes = {}                                  # 空 dict
recipes["Pizza"] = ["Dough", "tomatoes", "cheese"]   # 键 "Pizza" 是运行时写进去的
recipes
# {'Pizza': ['Dough', 'tomatoes', 'cheese']}

recipes["Pasta"] = ["Penne", "tomatoes"]
for name in recipes:                          # 遍历键
    print(name)                               # Pizza / Pasta
for ingredients in recipes.values():          # 直接遍历值
    print(ingredients)

每个条目是键(key)→ 值(value)的一对;键几乎可以是任何不可变对象(不限于字符串,整数也行:{1: "one"})。 给不存在的键赋值就是新增条目,这给 dict 一个列表没有的能力:按内容生长。

计数模式: 统计一段文本里每个词出现几次——「这个代码片段值得记住」:

text = "This is a test. This is only a test."
word_counts = {}
for word in text.split():
    if word in word_counts:
        word_counts[word] += 1     # 见过:加一
    else:
        word_counts[word] = 1      # 没见过:建条目
# {'This': 2, 'is': 2, 'a': 2, 'test.': 2, 'only': 1}

if word in word_counts 查键在不在——平均查找时间与 dict 大小无关(背后是哈希表实现——哈希是一种把任意数据快速映成固定编号的技巧; 这是「查找」这个词在数据结构里的技术含义:按名字找到值,且不论装了多少都几乎一样快)。 书里的俏皮话:「有个笑话,说 dict 是所有 LeetCode(编程题库)题的解——不完全对,但足以说明它多重要。」11

为什么有效、边界在哪: dict 把「按名字找」从 if-else 长链里解放出来; 边界:遍历顺序、键必须不可变、以及 word_counts[word] 查不存在的键会报错(要用 in 先探,或第 06 章的 get——书里没讲,补充:.get(key, 默认) 取不到时返回默认,不报错)。

3.2 dict vs 变量 vs 类

dict 和普通变量都能「名字 → 值」,差别是名字的确定性:写代码时就知的固定几个字段, 用变量或类更直白(.name 永远比 ["name"] 可读,还记得第 02 章吗); 运行时才能数出来的条目集合,才是 dict 的主场。12

4. tuple:只读组合,为「一次返回多个值」而生

4.1 基本行为

解决什么问题: 一个函数想同时吐回两个结果——商和余数、面积和周长。

怎么做(主线例子): 内建函数 divmod 返回的就是 tuple:13

divmod(10, 3)          # (3, 1) —— 商 3,余 1;这个括号+逗号就是 tuple 字面量

quotient, rest = divmod(10, 3)   # 解包:左边几个名字,右边几项,一一对应
quotient             # 3

解包(unpacking) 是 tuple 的灵魂操作:把组合按位置拆开,每个值落进一个名字。 自定义函数一样用——return area, perimeter 返回的其实是 (area, perimeter) 这个 tuple, 调用处 area, perimeter = rectangle(3, 4) 当场拆开。tuple 括号可以省略,逗号才是本体。14

只读是硬约束:a[0] = 4 直接 TypeError: 'tuple' object does not support item assignment—— 和第 03 章「不可变」的契约完全同构:调用者拿到它,就确定它不会被半路改掉。15

4.2 与 list 的分工

书里把分工说得直白:list 装一组性质相同的对象(为了遍历它们);tuple 可以装不同的对象,但—— 「我建议你别到处传 tuple。要装不同的对象,就该建一个类。」 所以作者本人「几乎不用 tuple」, 留给它的正当场景基本只剩「函数返回多个值」这一处。16

两个实用提醒:

  • tuple 别太大:超过两三个元素就容易丢线索;「返回 tuple 往往说明函数干了太多事」——拿不准就返回一个值(或一个类)。
  • 第 04 章 zip 的产物是 tuple 列表:list(zip([1,2,3],[4,5,6])) → [(1,4),(2,5),(3,6)]。

5. 作者的判断与证据

书里给了证据的: 所有代码示例可直接运行;dict 计数模式是完整可复用的模板;11 TypeError 报错信息展示了只读语义。15

作者的判断(立场):

  • 「字符串只存文本、其余代码零字符串逻辑」——强度很高,方向正确;真实代码里字符串工具函数(清洗、格式化)不可避免,他的意思更接近「业务逻辑别建立在解析字符串上」;
  • 「别传 tuple,要装异质对象就用类」——和他「别用 []、别切片」一脉相承的强默认;
  • dict「查找平均常数时间」是机制事实,可直接信任。11

判断(我们的,不是书里的): 这一章三个容器对应三种「信息形状」——文本(给人读)、映射(按名查)、记录(成组返回)。 作者没说的第四 shapes 是记录该升级成类:当 tuple/dict 开始被传来传去、字段名靠记,就是升级信号。 他的「几乎不用 tuple」对初学者是安全的默认,代价是错过解包这个 Python 里非常好用的语法糖——解包值得学,传大 tuple 值得戒。 如果错,会错在: 数据科学代码里,一行 name, value = pair 式的轻量解包无处不在, 「别用 tuple」如果理解成「别解包」,就把孩子和水一起倒了——他否定的只是「把 tuple 当数据结构传来传去」。

6. 边界与局限

  • 字符串方法只示范了 split/拼接,strip、join、startswith 等常用件没提(第 14 章 CSV 例子里倒是用了 strip);
  • dict 的 get、items()、字典推导式没讲——for k, v in d.items() 是日常高频,读者需自查;
  • 书里 contains 示例是错的(已在本章第 2.4 节勘误);
  • tuple 的具名替代 NamedTuple/dataclass(第 11 章的 @dataclass 才是作者的答案)在本章没点名衔接。

7. 可带走的

本章主线例子,一行写完: 字符串 f"I have {quantity} {fruit_name}" 织值入文; dict word_counts[word] += 1(不存在则建条目)统计词频; tuple quotient, rest = divmod(10, 3) 一次返回、当场解包。61113

  1. 字符串只存文本;拿它存结构化信息是头号反模式——该用类、enum 或 JSON/XML。
  2. f-string 是拼文本的默认写法:f"...{变量}...";读入的字符串数据要当场转成正确类型。
  3. 特殊字符三变体:f"" 织值、"""...""" 多行、r"" 原样反斜杠。
  4. dict = 运行时才确定键名的「名字→值」;计数模式(if in: +=1 else: =1)背下来。
  5. dict 查找平均与大小无关——可以大胆用在性能关键处。
  6. tuple = 只读组合;就留给「函数返回多个值 + 解包」,别当数据结构传来传去。
  7. 容器选择问一句:「这个东西在问题里扮演什么角色?」角色决定编制,不是功能多少。

8. 原文地图

主题 原书章 原文位置
字符串定义、引号 Strings text/14-fm-strings.txt:15(搜「quotation marks」)
f-string 主线例子 Strings text/14-fm-strings.txt:26(搜「My name is」) · text/14-fm-strings.txt:74(搜「I have」)
三种变体(f/多行/原始) Strings text/14-fm-strings.txt:63(搜「Various Types」) · text/14-fm-strings.txt:79(搜「multiline」) · text/14-fm-strings.txt:85(搜「raw string」)
特殊字符与 SQL 注入类比 Strings text/14-fm-strings.txt:52(搜「SQL injection」)
「字符串只存文本」反模式警告 Strings text/14-fm-strings.txt:34(搜「Using of Strings」)
字符串操作与 contains 勘误 Strings text/15-fm-dicts.txt:1(搜「String Operations」) · text/15-fm-dicts.txt:35(搜「contains」)
dict 定义与动态键名 Dicts text/15-fm-dicts.txt:50(搜「dynamically」)
dict 遍历 keys/values Dicts text/15-fm-dicts.txt:66(搜「recipes[”Pasta”]」) · text/15-fm-dicts.txt:82(搜「values()」)
词频计数模式 Dicts text/15-fm-dicts.txt:93(搜「frequency」)与 text/16-fm-tuples.txt:1(搜「word_counts」)
查找常数时间、LeetCode 笑话 Dicts text/16-fm-tuples.txt:16(搜「constant」)
tuple 定义与 divmod Tuples text/16-fm-tuples.txt:24(搜「inconspicuous」) · text/16-fm-tuples.txt:43(搜「divmod」)
解包与自定义返回 Tuples text/16-fm-tuples.txt:52(搜「Unpacking」) · text/16-fm-tuples.txt:62(搜「rectangle」)
tuple 别太大、作者几乎不用 Tuples text/16-fm-tuples.txt:72(搜「too large」) · text/16-fm-tuples.txt:97(搜「hardly ever use」)
list vs tuple 分工 Tuples text/16-fm-tuples.txt:76(搜「Tuples vs. Lists」)

  1. 出处:「Strings」第 14–17 段(text/14-fm-strings.txt:14,搜「text modules」)。 ↩

  2. 出处:「Dicts」第 50–57 段(text/15-fm-dicts.txt:50,搜「dynamically」)。 ↩↩

  3. 出处:「Tuples」第 24–26 段(text/16-fm-tuples.txt:24,搜「inconspicuous」)。 ↩

  4. 出处:「Dicts」文件开头「String Operations」节(text/15-fm-dicts.txt:6,搜「concatenate」;第 11 段,搜「not particularly useful」;第 34 段,搜「contains」)。勘误:contains 不是 Python 字符串方法,正确写法是 "l" in "Hello"。 ↩↩

  5. 出处:「Lists」第 251–260 段(「Strings vs. Lists」框,text/13-fm-lists.txt:252,搜「list of characters」)。 ↩

  6. 出处:「Strings」第 21–32 段(text/14-fm-strings.txt:26,搜「My name is」)。 ↩↩

  7. 出处:「Strings」第 41–45 段(text/14-fm-strings.txt:41,搜「can be converted into」)。 ↩

  8. 出处:「Strings」第 48–61 段(text/14-fm-strings.txt:52,搜「SQL injection」;第 58 段,搜「vice versa」)。 ↩

  9. 出处:「Strings」第 63–89 段(text/14-fm-strings.txt:79,搜「multiline string」;第 86 段,搜「raw string」)。 ↩

  10. 出处:「Strings」第 35–39 段(加框「Using of Strings」,text/14-fm-strings.txt:35,搜「antipattern」)。 ↩

  11. 出处:「Dicts」第 91–98 段(text/15-fm-dicts.txt:93,搜「frequency」)与「Tuples」文件开头(text/16-fm-tuples.txt:16,搜「constant」;第 17 段,搜「LeetCode」)。 ↩↩↩↩

  12. 出处:「Dicts」第 88–90 段(text/15-fm-dicts.txt:89,搜「enormously useful tool」)。 ↩

  13. 出处:「Tuples」第 41–57 段(text/16-fm-tuples.txt:43,搜「divmod」)。 ↩↩

  14. 出处:「Tuples」第 59–71 段(text/16-fm-tuples.txt:62,搜「rectangle」;「括号可省」见第 61、70 段)。 ↩

  15. 出处:「Tuples」第 34–39 段(text/16-fm-tuples.txt:39,搜「does not support item assignment」)。 ↩↩

  16. 出处:「Tuples」第 94–97 段(text/16-fm-tuples.txt:97,搜「hardly ever use」)。 ↩