Skip to content

第 05 章 · 字符串与格式化输出 ​

这一章要解决什么:把电芯的编号、批次和容量打印成一张对齐、别人能直接读的小表。 这一章要打掉两个坑:字符串方法全都返回新串、原串不动;以及最让人抓狂的中文全角标点。

本章你需要完成的代码:demos/ch05-string.py

5.1 字符串就是一串字符 ​

python
cell_id = "C001"
batch = "A"

字符串(str)是一串有序的字符。关键词是"有序" —— 因为有序,所以能按下标取、能切片 (和列表同一套规则,第 06 章会看到完全一样的写法)。

你看到的Python 眼里
"C001"4 个字符:C、0、0、1
" C001 "8 个字符,前后各两个空格(空格也是字符)
""空字符串,长度 0,也是合法的字符串

字符串是不可变的(immutable):一旦创建,里面的字符就改不了。这一条会在 5.6 节变成真实的行为差异 —— 所有"看起来在改字符串"的方法,其实都返回了一个新字符串。

5.2 引号与转义 ​

python
print('单引号 "内容" 双引号都可以')
print("换行用 \\n,制表用 \\t:")
print("第一行\n第二行\t← Tab 之后")
print("原始字符串 r'' 里 \\n 不再是换行:", r"C:\Users\jini\data")
写法效果
"abc" / 'abc'两者完全等价,"C001" == 'C001' 是 True
\n / \t换行 / 制表符(Tab)
\\一个反斜杠要写成两个
r"..."原始字符串:里面的 \ 不再是转义符

引号只要成对,单双都行;引号里要放同种引号就用 \ 转义:"他说 \"你好\""。

为什么路径一定要加 r:\t 是 Tab、\U 是 Unicode 转义的开关。 你写 "C:\user\temp",Python 会把 \t 变成一个 Tab 字符 —— 路径废了,而且不一定报错。 看到路径就加 r,这是肌肉记忆。(Linux / macOS 用 /,不涉及这个问题。)

5.3 三引号:写多行文本 ​

python
report = """批次: A
电芯: C001
结论: 健康
"""
print(report, end="")

"""...""" 里的换行原样保留,不用写 \n。两个正式用途: 多行文本(报告片段、SQL、HTML 模板)和文档字符串 docstring(函数底下第一段字符串, help() 会显示它,第 10 章)。

注意 end="":print 默认在末尾补换行,而三引号字符串自带了结尾换行, 不加 end="" 就会多出一个空行。

5.4 索引与切片:和列表同一套规则 ​

python
cell_id = "C001"
print(len(cell_id))       # 4
print(cell_id[0])         # 'C'   ← 第 1 个,下标 0
print(cell_id[-1])        # '1'   ← 最后一个
print(cell_id[1:])        # '001' ← 从下标 1 到结尾
print(cell_id[:1])        # 'C'   ← 只要第 1 个

把字符串想成一排字符,每个字符下面有一个编号:

text
 C   0   0   1
 0   1   2   3     ← 正向下标
-4  -3  -2  -1     ← 反向下标
写法结果记忆点
s[0]第 1 个字符下标从 0 开始
s[-1]最后 1 个字符负数从右往左数
s[1:]从第 2 个到结尾起点省略默认 0
s[1:3]下标 1、2左闭右开,右端取不到

切片"左闭右开"是本书出现频率最高的三个坑之一(第 06 章列表、第 19 章 NumPy 会原封不动再讲一遍)。 字符串和列表的切片唯一区别:caps[1:3] 得到列表,"C001"[1:3] 得到字符串 —— 类型跟着原对象走。

5.5 拼接与重复:+ 和 * ​

python
print("用 + 拼接:", "电芯" + cell_id)              # 电芯C001
print("用 * 重复:", cell_id * 3)                   # C001C001C001
print("推荐用 f-string:", f"电芯{cell_id}(批次{batch})")
运算作用注意
"a" + "b"拼接两边必须都是字符串,"a" + 1 报 TypeError
"a" * 3重复'aaa';不是乘法
"a" "b"字面量自动拼接只对写死在代码里的字符串生效

* 是重复不是乘法,这是第 04 章埋下的伏笔:

python
raw = "0.9921"          # 从 CSV 里读出来的是字符串
print(raw * 2)          # '0.99210.9921'  ← 不报错!但完全不是你要的
print(float(raw) * 2)   # 1.9842          ← 先转类型才对

这是"静默失败"的典型:程序不崩,结果错得离谱。凡是看到"数字"长得像字符串,先 float()。 推荐 f-string 而不是一串 +:+ 容易漏空格、容易把数字和字符串加起来报错,长表达式还不好折行。

5.6 常用方法:全都返回新字符串 ​

python
s = "  C001,Batch-A  "
print(s.strip())                  # 'C001,Batch-A'
print(s.strip().split(','))       # ['C001', 'Batch-A']
print(s.strip().upper())          # 'C001,BATCH-A'
print(s.strip().replace('-', '_'))# 'C001,Batch_A'
print(cell_id.startswith('C'))    # True
print(s.find(','))                # 6
print(s)                          # '  C001,Batch-A  ' ← 原串一点没变!
方法作用返回
s.strip()去掉首尾空白(lstrip/rstrip 去一侧)新字符串
s.split(',')按分隔符切开列表 ['C001', 'Batch-A']
s.replace('a','b')替换全部(不是正则)新字符串
s.upper() / s.lower()转大写 / 小写新字符串
s.startswith('C')判断开头(endswith 判断结尾)bool
s.find(',')第一次出现的下标,找不到返回 -1int
','.join(['a','b'])用分隔符把列表拼成字符串新字符串
点开:strip() 不改原串 —— 为什么说这是最重要的一条
python
s = "  C001  "
s.strip()
print(repr(s))          # '  C001  '  ← 白算了!要写 s = s.strip()

所有字符串方法都不会改原变量,因为字符串不可变(第 03 章"变量是标签"的直接后果): s.strip() 造了一个新对象 'C001',但没人给它贴标签,随即被回收。

这条规律同样适用于 round(x, 2)(第 04 章)、sorted(x)(第 06 章)—— 都返回新对象。 反过来,list.append() 这类返回 None 的方法才是"原地修改":

返回什么行为例子
返回新对象原对象不变s.strip()、sorted(x)、x.copy()
返回 None原地修改了lst.append()、lst.sort()、lst.pop()

find 返回 -1 也要留意:if s.find(',') 在找不到时是 -1(真值!), 判断"有没有"要用 if ',' in s:。

split 和 join 是一对:line.split(',') 把一行文本变成列表(第 12 章读 CSV 的核心动作), ",".join(parts) 再拼回去。注意 "a b".split() → ['a','b'](连续空白当一个分隔符), 而 "a b".split(' ') 会留下空串 —— 处理人工输入优先用不带参数的 split()。

5.7 f-string 格式化迷你语言 ​

前面几章已经在用 f"{soh:.2f}",现在把它讲全。格式符的结构是固定的:

text
{ 值 : [填充字符][对齐符][宽度][.精度][类型] }
python
caps = {"C001": 2.50, "C002": 2.29, "C003": 2.314159}
print(f"{'电芯':<6}{'容量':>10}{'SOH':>8}")
print("-" * 24)
for cid, cap in caps.items():
    print(f"{cid:<6}{cap:>10.3f}{cap / 2.5:>8.1%}")

输出:

text
电芯            容量     SOH
------------------------
C001       2.500  100.0%
C002       2.290   91.6%
C003       2.314   92.6%
写法输出说明
{x:.2f}0.50两位小数
{x:.1%}50.0%百分比(自动乘 100 加 %)
{x:>8} / {x:<8} / {x:^8} ab / ab / ab 右 / 左 / 居中,总宽 8
{x:08.2f}00003.14用 0 填充到 8 位
{x:,}1,234,567千分位
{x:+.2f}+3.14强制显示正负号

三个必须记住的点:

  1. 顺序不能乱:写成 {x:.3f>8} 会直接报 ValueError;
  2. 宽度是"总宽度",不是"补几个空格"("C001" 已占 4 格,所以只补 4 个空格);
  3. 一张排版单的记忆法:{ '电芯' : < 6 } = "把『电芯』装进宽 6 格的框里,靠左放"。 数字默认右对齐,因为个位对齐才能一眼比大小。

中文对齐会"看起来不齐":中文字在终端占 2 个显示宽度,但 len('电芯') 是 2。 严格排版要用 tabulate 之类的库。

5.8 f-string 的 = 调试语法 ​

python
n_cycles = 862
print(f"{n_cycles=}")        # n_cycles=862
print(f"{n_cycles * 2=}")    # n_cycles * 2=1724
print(f"{soh=:.4f}")         # soh=0.9160  ← 加格式符也有效

Python 3.8+ 支持在 f-string 里加 =:它会自动把表达式本身也打印出来。 以前排查问题要写 print("n_cycles =", n_cycles),变量名是手打的字符串,改一次变量名就忘了改字符串, 日志立刻开始骗人。f"{x=}" 的"名字"是从代码里取的,永远不会和值对不上。

5.9 完整示例与输出 ​

bash
cd code
python demos/ch05-string.py

真实输出(节选):

text
=== ① 引号 ===
单引号 "内容" 双引号都可以
换行用 \n,制表用 \t:
第一行
第二行	← Tab 之后
原始字符串 r'' 里 \n 不再是换行: C:\Users\jini\data

=== ③ 索引与切片 ===
cell_id = 'C001',长度 4
cell_id[0] = 'C'     ← 第一个字符
cell_id[-1] = '1'   ← 最后一个字符
cell_id[1:] = '001'   ← 从第 2 个到结尾

=== ④ 拼接与重复 ===
用 * 重复: C001C001C001   ← 不是编号翻倍!字符串乘法是重复

=== ⑤ 常用方法(字符串不可变) ===
原始      : '  C001,Batch-A  '
strip()   : 'C001,Batch-A'
split(',') : ['C001', 'Batch-A']
find(',')  : 6
注意:所有方法都返回新字符串,原变量没变 → '  C001,Batch-A  '

=== ⑥ f-string 格式化迷你语言 ===
电芯            容量     SOH
------------------------
C001       2.500  100.0%
C002       2.290   91.6%
C003       2.314   92.6%

=== ⑦ f-string 的调试语法 ===
n_cycles=862   ← 自动打印「表达式=值」,排查问题时省事

=== ⑧ 中文标点是新手第一大坑 ===
  错:print("你好")   ← 全角括号
  错:print("你好“)      ← 全角引号
  对:print("你好")

5.10 常见坑 ​

坑 ①:中文全角标点 —— 新手第一大语法错误。

python
print("你好")        # ❌ SyntaxError: invalid character '('
print("你好“)          # ❌ 引号没配对

编辑器里 ( ) , “ ” : 和 ( ) , " " : 长得极像,但 Python 只认半角。 中文输入法开着写代码 = 迟早踩。

坑 ②:以为 strip() 改了原串。 要写 s = s.strip(),理由见 5.6。

坑 ③:split() 返回的是列表,不是字符串。

python
parts = "C001,2.50".split(',')
parts.upper()            # ❌ AttributeError: 'list' object has no attribute 'upper'
parts[0].upper()         # ✅ C001

方法用错对象时,先 print(type(x))。

坑 ④:数字字符串做运算。 "2.50" + "2.50" 是 '2.502.50'、"2.50" * 2 是 '2.502.50', 都不报错。要算数先 float()。

坑 ⑤:find 返回 -1 被当成真值。

python
if s.find("A"):          # -1 是真值!条件成立 → 逻辑全反

用 if "A" in s: 或 if s.find("A") != -1:。

坑 ⑥:f-string 里嵌套引号。 Python 3.12 之前内外不能同种引号, 写 f"{cell['id']}"(外双内单)最安全。

坑 ⑦:拼接时忘了类型统一。 "SOH = " + 0.92 报 TypeError; 用 f"SOH = {0.92}" 一次解决。

本章练习 ​

练习 1:把电芯信息排版成一张三列表格

用 f-string 的格式化迷你语言打印一张表:

  • 三列:编号(左对齐宽 8)、容量(右对齐宽 12、保留 3 位小数)、SOH(右对齐宽 8、百分比 1 位);
  • 表头用同样的宽度写;分隔线用 "-" * 32 生成;
  • 数据两行:C001 2.50、C002 2.29(额定 2.5 Ah);
  • 最后单独打印组合编号,形如 A-C001。

期望输出(节选):

text
电芯           容量 (Ah)     SOH
--------------------------------
C001           2.500  100.0%
C002           2.290   91.6%
组合编号:A-C001
参考答案
python
cell_id = "C001"
batch = "A"
cap_rated = 2.5
rows = [("C001", 2.50), ("C002", 2.29)]

# ① 表头:和数据行用完全相同的宽度,这是"对齐"的全部秘密
print(f"{'电芯':<8}{'容量 (Ah)':>12}{'SOH':>8}")
# ② 分隔线:用 * 重复,别手打 32 个减号
print("-" * 32)

# ③ 数据行:对齐符、宽度、精度写在同一个格式符里
for cid, cap in rows:
    print(f"{cid:<8}{cap:>12.3f}{cap / cap_rated:>8.1%}")

# ④ 组合编号:f-string 直接拼,不用 +
print(f"组合编号:{batch}-{cell_id}")

要点解释:

  1. 对齐的秘诀是"表头和数据用同一个宽度"。列宽 8 就都写 8。 < > 后面是总宽度(含数字本身),不是"补几个空格"。
  2. 格式符顺序固定:{值:[填充][对齐][宽度][.精度][类型]}, 写成 {x:.3f>8} 会报 ValueError: Invalid format specifier。
  3. :.1% 自动乘 100:2.50 / 2.5 = 1.0 → 100.0%。
  4. 中文列宽的小陷阱:"电芯" 的 len() 是 2,但显示占 4 格, 中英混排会"看起来差一点",严格排版要用 tabulate。

完整练习在 exercises/ex04-05-string (仓库的 solution.py 用的是 :<10 / :>12,宽度不同、输出也就略不同 —— 宽度是排版选择,不是语法)。

练习 2:从一行文本里抽出字段

给定一行"从 CSV 里读出来"的字符串 line = " C001,2.50,0.92 ",依次打印:

  1. 用 strip() 去掉首尾空格后的字符串;
  2. 用 split(',') 切出来的列表;
  3. 把第 2 个字段转成 float 后乘 1000(Ah → mAh),形如 2500.0 mAh;
  4. 逗号第一次出现的下标(用 find)。

期望输出:

text
'C001,2.50,0.92'
['C001', '2.50', '0.92']
2500.0 mAh
逗号第一次出现的下标:4
参考答案
python
line = "  C001,2.50,0.92  "

# ① strip 返回新串,必须接收返回值
clean = line.strip()
print(repr(clean))

# ② split 返回的是列表
parts = clean.split(',')
print(parts)

# ③ 从"文件"读出来的一定是字符串,算数前必须转类型
cap_ah = float(parts[1])
print(f"{cap_ah * 1000} mAh")

# ④ find 返回下标(找不到是 -1)
print(f"逗号第一次出现的下标:{clean.find(',')}")

三个要点:

  1. 每个方法都返回新对象:clean = line.strip()、parts = clean.split(',')。 直接写 line.strip() 不接收返回值 = 白算 —— 这是本章最容易犯的错。
  2. float(parts[1]) 不能省:parts[1] 是 '2.50',乘以 1000 会得到 '2.502.50...'(重复 1000 次!),不报错但完全错。
  3. 打印字符串用 repr() 或 !r,能看见引号和空格 —— 调试"空格到底删掉没有"时,print(s) 根本看不出来。
练习 3:预测输出(不要运行,先想)
python
s = "C001"
print(s[1:3])
print(s[-2:])
print(s * 2)
print(len(s))

raw = "3.5"
print(raw + raw)
print(float(raw) + float(raw))

x = 0.916
print(f"{x:.1%}")
print(f"{x=}")

要求:先写下每一行的输出,再运行验证。不一致的地方,说出原因。

参考答案

预测输出:

text
00
01
C001C001
4
3.53.5
7.0
91.6%
x=0.916
表达式结果为什么
s[1:3]'00'左闭右开,取下标 1、2,不含 3
s[-2:]'01'从倒数第 2 个到结尾 —— 负下标从右往左数(画一遍下标就不会错)
s * 2'C001C001'* 对字符串是重复
raw + raw'3.53.5'+ 对字符串是拼接,不是加法
float(raw)+float(raw)7.0先转类型才是数学加法
f"{x:.1%}"'91.6%'自动乘 100 加 %,保留 1 位
f"{x=}"'x=0.916'= 语法自动带上表达式名

最容易错的两行:

  • s[-2:] 是 '01' 不是 'C0':负下标从右往左数,-2 指倒数第 2 个字符。
  • raw + raw 是拼接:它和 float(...) + float(...) 都合法、都不报错, 但一个是字符串拼接、一个是数值加法 —— 同一段代码都不报错,结果却完全不同, 这正是"读进来的数字必须先转类型"的原因。

面向 AI 开发:字符串是"模型与数据之间的接口" ​

在 AI / 数据分析项目里,字符串几乎不是"给人看的文字",而是数据的载体和接口:

① 日志与实验结果全是字符串。

python
line = "epoch=12 train_loss=0.2341 val_loss=0.3102"
d = dict(kv.split("=") for kv in line.split())
print(float(d["val_loss"]))          # 0.3102

训练日志、实验记录都要用 split / find / startswith 解析。 f"{x=}" 就是为这个场景生的:print(f"{loss=:.4f}") 直接产出 loss=0.2341, 可以整段丢进实验记录,不会出现"变量名和值对不上"的日志。

② 提示词(prompt)就是字符串拼接。

python
prompt = f"""你在检查一批电池数据。
批次: {batch}
SOH 下限: {soh_min:.2f}
请指出哪几支电芯需要复检。
"""

三引号(多行)+ f-string(插值)+ 格式符(精度)是一套组合拳。 数值一定要统一精度:{soh:.2f} 比 {soh} 稳定得多,否则同一个值可能打印成 0.9199999999999999,白白让日志和模型困惑。

③ 字段值往往是字符串,类型转换是第一道工序。

场景典型字符串要做什么
读 CSV"0.9921"float(),否则 * 2 变重复
读配置"true" / "0.1"== "true"、float()
命令行参数"42"int()
JSON / API 返回"2023-01-01"pd.to_datetime()(第 29 章)

④ 文件名用 f-string 生成,是批量导出的标准动作。

python
for ch in (1, 2, 3):
    out = f"soh_ch{ch:02d}.csv"      # soh_ch01.csv ← 02d 自动补零,文件排序才对

没有 02d,soh_ch10.csv 会排在 soh_ch2.csv 前面(字符串排序按字符比较)—— 这是批量导出时的经典坑。

一句话总结:字符串看似"只是打印好看一点",其实同时管着 输入解析(split/find/strip)、输出规范(f-string 格式符)、接口拼接(prompt/路径) —— 而这三件事里的每一个错误,通常都不报错。


下一章:第 06 章 · 列表:存放一支电芯的循环容量