第 06 章 · 列表:存放一支电芯的循环容量
这一章要解决什么:把一支电芯每一圈的容量按顺序存起来,并按"第几次循环"准确取出来。 这一章要打掉三个坑:下标从 0 开始、切片不含右端、
b = a不是复制。
本章你需要完成的代码:demos/ch06-list.py
6.1 为什么需要列表
到第 05 章为止,一个变量只能装一个值:
cap1 = 2.50
cap2 = 2.46
cap3 = 2.42如果一支电芯跑 800 圈,你不可能写 800 个变量名。列表(list)就是"一串值用一个名字装":
caps = [2.50, 2.46, 2.42, 2.38, 2.33]
print(len(caps)) # 5 有几个?
print(caps[0]) # 2.5 第 1 次是多少?
print(caps[-1]) # 2.33 最后一次是多少?
print(caps[:3]) # [2.5, 2.46, 2.42] 前 3 次是多少?
print(max(caps), sum(caps) / len(caps)) # 最大 / 平均列表是本书后面所有数据结构的起点:第 17 章的 NumPy 数组是"能整列运算的列表", 第 23 章的 DataFrame 是"带列名的列表集合"。 列表里现在要用 for 完成的事,将来都是一行代码 —— 但前提是先理解列表的规则。
6.2 创建与查看
caps = [2.50, 2.46, 2.42, 2.38, 2.33] # 方括号 + 逗号
print(caps, len(caps)) # [2.5, 2.46, 2.42, 2.38, 2.33] 5
empty = [] # 空列表,等着往里放东西
zeros = [0] * 5 # [0, 0, 0, 0, 0] ← 和字符串的 * 同一个意思| 写法 | 得到 | 说明 |
|---|---|---|
[1, 2, 3] | 三个元素的列表 | 元素用逗号分隔 |
[] | 空列表 | len([]) 是 0,布尔值是 False(第 08 章) |
[0] * 5 | [0, 0, 0, 0, 0] | * 是重复;注意 [0] 的方括号不能省 |
list("C001") | ['C', '0', '0', '1'] | 把一串字符拆成列表 |
列表的打印会自己"美化":2.50 打印成 2.5,字符串打印时带引号。 这是"你在看数据结构"而不是"在看文本"的信号。
6.3 索引:0 基是最容易错的地方
caps = [2.50, 2.46, 2.42, 2.38, 2.33]
print(caps[0]) # 2.5 第 1 次
print(caps[2]) # 2.42 第 3 次 ← 不是 3!
print(caps[-1]) # 2.33 最后一次
print(caps[-2]) # 2.38 倒数第二次把列表在脑子里摊开,每个元素下面写编号:
下标: 0 1 2 3 4
值: 2.50 2.46 2.42 2.38 2.33
负下标: -5 -4 -3 -2 -1| 人的说法 | 下标 | 写法 |
|---|---|---|
| 第 1 次 | 0 | caps[0] |
| 第 3 次 | 2 | caps[2] |
| 第 n 次 | n-1 | caps[n - 1] |
| 最后一次 | -1 | caps[-1](不用数长度) |
转换口诀:caps[i] 是"第 i+1 次"。 看到 caps[3] 就念"第 4 次"。
为什么偏偏从 0 开始:下标在底层表示"偏移量" —— 首元素的地址偏移是 0。 这个设计从 C 语言一路传下来,Python、NumPy、Pandas、PyTorch 全都一样。 负下标的好处是 caps[-1] 永远指"最后一个",这支电芯跑 5 圈还是 800 圈,代码都不用改。
6.4 切片:左闭右开
print(caps[:3]) # [2.5, 2.46, 2.42] 前 3 次
print(caps[1:4]) # [2.46, 2.42, 2.38] 下标 1、2、3
print(caps[-2:]) # [2.38, 2.33] 最后 2 次
print(caps[::2]) # [2.5, 2.42, 2.33] 每隔一个
print(caps[:]) # 整个列表(浅拷贝)切片语法是 caps[起点:终点:步长],三个位置都可以省略:
| 写法 | 含义 | 结果 |
|---|---|---|
caps[:3] | 从头到下标 3(不含) | 下标 0,1,2 |
caps[1:4] | 下标 1 到 4(不含) | 下标 1,2,3 |
caps[3:] | 从下标 3 到结尾 | 下标 3,4 |
caps[-2:] | 最后 2 个 | 与长度无关 |
caps[::2] | 每 2 个取 1 个 | 0,2,4 |
caps[::-1] | 倒序 | 反向列表 |
"左闭右开"记法:终点写的是"我要停在哪里",不是"我要取几个"。 所以 caps[1:4] 取 3 个元素,长度等于 4 - 1。
点开:为什么是左闭右开?(理解了就不用死记)
① 长度直接相减:caps[1:4] 的长度 = 4 - 1 = 3。若右端也算,长度就是 4 - 1 + 1,每次都要 +1。
② 不重叠地切开:caps[:3] + caps[3:] 恰好拼回整个列表,没有元素被算两次。 这在"分块处理"(第 28 章的分组)里是关键性质。
③ 和 range 完全一致:range(1, 4) 是 1, 2, 3(第 07 章)。 切片和 range 用同一套规则,学一次管两处 —— 这不是巧合,是刻意设计。
怎么验证自己写对了:print(len(caps[1:4])),应该是 3。拿不准就打印长度,比在脑子里数靠谱。
6.5 增删改:append / pop / insert / del
caps = [2.50, 2.46, 2.42, 2.38, 2.33]
caps.append(2.29) # 末尾追加 → [2.5, 2.46, 2.42, 2.38, 2.33, 2.29]
caps[0] = 2.51 # 直接改第 1 个 → [2.51, 2.46, 2.42, 2.38, 2.33, 2.29]
removed = caps.pop() # 弹出末尾,返回被弹出的值 → 2.29
caps.insert(0, 2.50) # 插到最前面 → [2.5, 2.51, 2.46, 2.42, 2.38, 2.33]
del caps[0] # 按位置删 → [2.51, 2.46, 2.42, 2.38, 2.33]| 操作 | 作用 | 返回值 |
|---|---|---|
caps.append(x) | 末尾追加 | None |
caps.pop() / caps.pop(0) | 弹出末尾 / 第 1 个 | 被弹出的元素 |
caps.insert(i, x) | 插到下标 i 前 | None |
caps[i] = x | 改第 i 个 | —— |
del caps[i] | 删第 i 个 | —— |
caps.remove(x) | 删掉第一个等于 x 的元素 | None |
列表是"可变对象":这些都是原地修改,改的就是那个列表对象本身。
append 返回 None —— 本章第一个静默 bug
demo = [1, 2]
print(demo.append(3)) # None ← 打印出来的是返回值
print(demo) # [1, 2, 3] ← 列表自己变了所以千万别写:
caps = caps.append(2.29) # ❌ 列表确实变了,但 caps 被重新绑到 None 上!
print(caps) # None
print(len(caps)) # TypeError: object of type 'NoneType' has no len()为什么这么设计:append 是"命令"(去做一件事),不是"计算"(算出个结果)。 返回 None 就是在说"我没有结果给你"。
| 返回值 | 行为 | 例子 |
|---|---|---|
None | 原地修改 | append、insert、remove、sort |
| 新对象 / 值 | 原对象不变 | sorted、copy、pop(返回被删的元素) |
6.6 排序与统计:内置函数,不用写循环
print(sorted(caps)) # 升序,返回新列表
print(sorted(caps, reverse=True)) # 降序
print(max(caps), min(caps), sum(caps))
print(caps.count(2.42)) # 出现几次
print(caps.index(2.42)) # 第一次出现的下标
print(round(sum(caps) / len(caps), 4)) # 均值 2.3967| 函数 | 作用 | 注意 |
|---|---|---|
sorted(x) | 排序 | 返回新列表,原列表不动 |
x.sort() | 原地排序 | 返回 None,原列表被改 |
max / min / sum | 最大 / 最小 / 求和 | 空列表 max 会报 ValueError |
x.count(v) | v 出现几次 | 找不到返回 0 |
x.index(v) | v 第一次出现的下标 | 找不到报 ValueError(不是 -1!) |
sorted(caps) 和 caps.sort() 的区别,是 5.6 节那条规律的又一次出现:
a = [3, 1, 2]
b = sorted(a) # a 不变,b = [1, 2, 3]
a.sort() # a 变成 [1, 2, 3],返回值是 None平均值没有内置函数,最直接的写法是 sum(caps) / len(caps)。 这就是**"累加器模式"的最短形态**,第 07 章会把它展开成 for 循环 —— 看清"一行代码背后实际在做什么",是用好 NumPy 的前提。
6.7 混合类型的列表(为 NumPy 埋伏笔)
mixed = ["C001", 2.5, True, None]
for item in mixed:
print(f" {str(item):<8} → {type(item).__name__}")输出:
C001 → str
2.5 → float
True → bool
None → NoneTypePython 的列表允许混装,但混装会让后续计算频繁报类型错误:
mixed = [1, 2, "3"]
print(sum(mixed)) # TypeError: unsupported operand type(s) for +: 'int' and 'str'NumPy 的答案是"一个数组只能有一种类型"(第 17 章):np.array([1, 2.5, True]) 会把 int 和 bool 都升格成 float64。这带来了速度,也带来了"字符串一混进来,整列就废了"的风险。 工程纪律:一个列表只放一种东西。 编号放一个列表、容量放一个列表。
6.8 可变对象的陷阱:b = a 不是复制
这是全章最重要的一节,也是第 03 章"变量是标签"的实测现场。
a = [1, 2, 3]
b = a # 不是复制!是两个名字贴同一个列表
b.append(4)
print(a, b) # [1, 2, 3, 4] [1, 2, 3, 4] ← a 也变了!a = [1, 2, 3] → a ──┐
├──> [1, 2, 3] (一个对象,两个标签)
b = a → b ──┘
b.append(4) → [1, 2, 3, 4] ← 改的是那个对象要真复制,明确说出来:
c = a.copy() # ✅ 真复制(浅拷贝)
c.append(5)
print(a, c) # [1, 2, 3, 4] [1, 2, 3, 4, 5] ← a 不受影响
print(a[:] is a) # False ← 切片 a[:] 也是浅拷贝,是新对象
print(a[:] == a) # True ← 内容相同,但不是同一个对象点开:is 和 == 在列表上到底差在哪
| 写法 | 问的问题 | 列表上的结果 |
|---|---|---|
a == b | 内容一样吗? | 元素逐个比较,内容相同就是 True |
a is b | 是同一个对象吗? | 只有指向同一块内存才 True |
a is None | 是不是那个唯一的 None | 判空专用(PEP 8) |
a = [1, 2, 3]
b = a
c = [1, 2, 3]
print(a == b, a == c) # True True ← 内容都一样
print(a is b, a is c) # True False ← 只有 b 是同一个对象为什么要区分:a == c 为 True 时,改 a 不会影响 c; 而 a is b 为 True 时,改 a 一定会影响 b。 排查"我没改它啊,怎么变了"这类 bug,第一个动作就是看两个名字是不是同一个对象。
浅拷贝的"浅"也是一层坑(第 19、30 章细讲):a.copy() 只复制最外层:
x = [[1, 2], [3, 4]]
y = x.copy()
y[0].append(99)
print(x) # [[1, 2, 99], [3, 4]] ← 里层还是共享的!要真深复制:import copy; copy.deepcopy(x)。本书的电池数据都是一维数值,copy() 就够。
6.9 越界:IndexError
caps = [2.50, 2.46, 2.42, 2.38, 2.33]
print(caps[99])
# IndexError: list index out of range合法下标:0 ~ len(caps) - 1(正向),或 -1 ~ -len(caps)(反向)。 5 个元素,合法下标就是 0~4 或 -1~-5,caps[5] 就已经越界。 排查顺序:print(len(caps)) → 确认最大合法下标 → 回头看你写的那个下标。
| 场景 | 错误写法 | 正确写法 |
|---|---|---|
| 循环里用 1 开始编号 | caps[i](i 从 1 开始) | caps[i-1] 或 enumerate(caps, start=1) |
| 想取"第 n 次" | caps[n] | caps[n-1] |
| 空列表取第 1 个 | caps[0] | 先判 if caps:(第 08 章) |
切片不会越界,这一点和索引不同:
print(caps[2:99]) # [2.42, 2.38, 2.33] 不报错,自动截到结尾
print(caps[99:]) # [] 也不报错,空列表为什么:切片的语义是"取一段范围",超出边界就"到此为止"; 索引问的是"第 99 个元素是谁",答不上来只能报错。这个差异在 NumPy 里同样成立(第 19 章)。
6.10 完整示例与输出
cd code
python demos/ch06-list.py真实输出(节选):
=== ① 创建与查看 ===
caps = [2.5, 2.46, 2.42, 2.38, 2.33] 长度: 5
=== ② 索引(0 基,最容易错) ===
第 1 次循环:2.5 Ah ← 下标 0
第 3 次循环:2.42 Ah ← 下标 2,不是 3!
最后一次 :2.33 Ah ← 负索引从右往左数
=== ③ 切片(左闭右开) ===
前 3 次 caps[:3] = [2.5, 2.46, 2.42]
第 2~4 次 caps[1:4] = [2.46, 2.42, 2.38] ← 下标 1,2,3
最后 2 次 caps[-2:] = [2.38, 2.33]
每隔一个 caps[::2] = [2.5, 2.42, 2.33]
整个列表 caps[:] = [2.5, 2.46, 2.42, 2.38, 2.33] (这是浅拷贝)
=== ④ 增删改 ===
append 之后: [2.5, 2.46, 2.42, 2.38, 2.33, 2.29] 长度: 6
pop() 返回 2.29,剩: [2.51, 2.46, 2.42, 2.38, 2.33]
del caps[0] 之后: [2.51, 2.46, 2.42, 2.38, 2.33]
=== ⑤ cap.append() 的返回值是 None ===
append 的返回值: None ← None!千万别 caps = caps.append(x)
但 demo 自己变了: [1, 2, 3]
=== ⑥ 排序与查找 ===
升序 sorted(caps): [2.33, 2.38, 2.42, 2.46, 2.51]
最大值: 2.51 最小值: 2.33 求和: 12.1 均值: 2.42
2.42 出现了几次: 1 2.42 第一次出现在下标: 2
=== ⑦ 混合类型的列表 ===
mixed = ['C001', 2.5, True, None]
C001 → str
2.5 → float
True → bool
None → NoneType
=== ⑧ 可变对象的陷阱 ===
b.append(4) 之后:a = [1, 2, 3, 4] b = [1, 2, 3, 4] ← a 也变了!
c = a.copy() 后 c.append(5):a = [1, 2, 3, 4] c = [1, 2, 3, 4, 5]
切片 a[:] 也是浅拷贝: False ← False 表示是两个对象
=== ⑨ 越界 ===
IndexError: list index out of range
合法下标:0 ~ 4 或 -1 ~ 5注意最后一行 0 ~ 4 或 -1 ~ 5:这是 demo 的一处笔误 —— 反向合法下标应该是 -1 ~ -5。 读示例代码也要带着怀疑,这种"输出自己不自洽"的地方一眼就该看出来。
6.11 常见坑
坑 ①:b = a 当成复制。 b = a 只是多贴一个标签,要复制写 b = a.copy()。 这是本书反复出现的第一大坑:第 17~19 章 NumPy 的"视图 vs 拷贝"、 第 27 章 Pandas 的 SettingWithCopyWarning,本质都是"你手上这个变量是不是独立的一份?"
坑 ②:caps = caps.append(x)。 返回 None,caps 直接变成 None。
坑 ③:把 caps[3] 说成"第 3 次"。 caps[3] 是第 4 次,口诀 caps[i] = 第 i+1 次。
坑 ④:切片终点写成"元素个数"。
caps[1:3] # 取 2 个(下标 1、2)
caps[1:4] # 取 3 个(下标 1、2、3)← 想取"第 2~4 次"要写这个拿不准就 print(len(切片))。
坑 ⑤:sort() 的返回值当成排序结果。 b = caps.sort() 得到 None,要用 b = sorted(caps)。
坑 ⑥:index() 找不到会报错,不是返回 -1。caps.index(9.99) 抛 ValueError;要判断"有没有"就用 if 9.99 in caps:。 字符串的 find() 返回 -1(第 05 章),列表的 index() 抛异常 —— 这两个别搞混。
坑 ⑦:remove(x) 删的是"第一个等于 x 的元素",不是下标。caps.remove(2.42) 删的是值为 2.42 的元素;del caps[2] 才是按下标删。
坑 ⑧:以为"切片不越界"= "随便写都安全"。 caps[2:99] 没问题,caps[99] 直接崩。
本章练习
练习 1:按循环次数取容量值
已知六次循环的容量:
caps = [2.50, 2.46, 2.42, 2.38, 2.33, 2.29]依次打印:第 1 次的容量、第 3 次的容量(先想清楚下标是几)、最后一次的容量(用负下标,不许数), 再打印列表长度和合法下标范围,并验证 caps[-1] == caps[len(caps)-1]。
期望输出:
第 1 次循环:2.5 Ah
第 3 次循环:2.42 Ah
最后一次循环:2.29 Ah
(校验)列表长度 6,合法下标 0~5
caps[-1] == caps[5] ? True参考答案
caps = [2.50, 2.46, 2.42, 2.38, 2.33, 2.29]
# ① 第 1 次 → 下标 0
print(f"第 1 次循环:{caps[0]} Ah")
# ② 第 3 次 → 下标 2(★ 本练习唯一的坑)
print(f"第 3 次循环:{caps[2]} Ah")
# ③ 最后一次 → 负下标 -1(和长度无关,最好用)
print(f"最后一次循环:{caps[-1]} Ah")
# ④ 用长度验证
n = len(caps)
print(f"(校验)列表长度 {n},合法下标 0~{n - 1}")
print(f" caps[-1] == caps[{n - 1}] ? {caps[-1] == caps[n - 1]}")要点解释:
caps[2]是第 3 次。写成caps[3]会得到2.38,而且不报错 —— 程序照跑、结果错了却看不出来,这就是这个坑危险的地方。caps[-1]等价于caps[len(caps)-1]。容量列表从 6 个变成 800 个,代码一个字都不用改。IndexError: list index out of range是越界的报错。6 个元素的最大合法下标是 5。- 打印列表是调试的第一手段:先
print(caps)看真实内容,再谈下标。
完整练习在 exercises/ex04-06-list。
练习 2:用切片切出三段容量
还是 caps = [2.50, 2.46, 2.42, 2.38, 2.33, 2.29],用切片(不许写循环、不许手打列表)打印:
- 前 3 次的容量;
- 第 2~4 次的容量,并打印长度,验证长度等于
4 - 1; - 最后 2 次的容量;
- 每隔一个取一个。
期望输出:
前 3 次: [2.5, 2.46, 2.42]
第 2~4 次: [2.46, 2.42, 2.38] (长度 3 = 4 - 1)
最后 2 次: [2.33, 2.29]
每隔一个: [2.5, 2.42, 2.33]参考答案
caps = [2.50, 2.46, 2.42, 2.38, 2.33, 2.29]
# ① 前 3 次:起点省略默认从 0 开始
print(f"前 3 次: {caps[:3]}")
# ② 第 2~4 次:起点下标 1,终点写 4(右端取不到)→ 取下标 1,2,3
mid = caps[1:4]
print(f"第 2~4 次: {mid} (长度 {len(mid)} = 4 - 1)")
# ③ 最后 2 次:负下标起点,省掉终点
print(f"最后 2 次: {caps[-2:]}")
# ④ 每隔一个:步长 2
print(f"每隔一个: {caps[::2]}")要点解释:
- 终点写的是"停在哪里",不是"取几个"。想要"第 2~4 次"就写
caps[1:4],不是caps[1:3]。 拿不准就print(len(...))。 - 三个位置都能省:
caps[:3](省起点)、caps[3:](省终点)、caps[::2](步长默认 1)。 - 切片返回新列表:改切片不影响原列表 —— 和 6.8 节的
b = a正好相反, 因为切片创建了新对象。caps[:]就是"整个列表的浅拷贝"。 caps[1:99]也不会报错,最多是取到结尾。
练习 3:预测输出(不要运行,先想)
a = [1, 2, 3]
b = a
b.append(4)
print(a, b)
print(a is b)
c = a.copy()
c.append(5)
print(a, c)
print(a[:] is a)
print(a[:] == a)
demo = [1, 2]
print(demo.append(3))
print(demo)要求:先写下每一行输出,再运行验证。不一致的地方,说明原因。
参考答案
预测输出:
[1, 2, 3, 4] [1, 2, 3, 4]
True
[1, 2, 3, 4] [1, 2, 3, 4, 5]
False
True
None
[1, 2, 3]| 行 | 输出 | 为什么 |
|---|---|---|
print(a, b) | [1,2,3,4] [1,2,3,4] | b = a 只是多贴一个标签,改的是同一个对象 |
print(a is b) | True | 同一个对象 |
print(a, c) | [1,2,3,4] / [1,2,3,4,5] | a.copy() 建了新对象,c 的修改不影响 a |
print(a[:] is a) | False | 切片产生新对象(浅拷贝) |
print(a[:] == a) | True | 新对象但内容相同 —— == 比内容、is 比身份 |
print(demo.append(3)) | None | append 原地修改,没有返回值 |
print(demo) | [1, 2, 3] | 但它确实改了列表本身 |
★ 把这张表记住,这是本章的总结:
b = a 两个名字 → 同一个对象 → 同生共死
c = a.copy() 新对象,内容相同 → 各过各的
a[:] 新对象(浅拷贝) → 各过各的
demo.append(3) 原地改,返回 None → 别接收返回值最深的一层:print(a, c) 和 print(a[:] == a) 都在说"内容一样", 但一个会互相影响、一个不会。判断标准不是"值等不等",而是"是不是同一个对象"(is)。 第 19 章 NumPy 的"视图 vs 拷贝"就是这条规则的高危版本:那里一个切片可能还是视图, 改一下原数组,整批数据全变。
面向 AI 开发:列表就是"一批样本"的最小形态
列表是 AI 项目里"数据集合"的第一个样子,理解它的边界,才知道为什么要升级到 NumPy 和 Pandas。
① 列表 = 一批样本,列表推导式 = 逐样本处理。
caps = [2.50, 2.46, 2.42] # 3 个样本的容量
sohs = [c / 2.5 for c in caps] # 每个样本做同一件事(第 11 章)
low = [c for c in caps if c / 2.5 < 0.85] # 筛出低健康度的神经网络训练时喂进去的就是"一批样本"(一个 batch)。 列表能装它们,但装不下一百万个 —— 这就是第 17 章 NumPy 存在的理由:
# 列表:整列运算必须写循环,慢
sohs = []
for c in caps:
sohs.append(c / 2.5)
# NumPy:一行,底层是 C 循环,快几十倍(第 20 章)
sohs = np.array(caps) / 2.5② 列表是"数据集"和"标签集"的天然配对,靠 zip 并排遍历(第 07 章)。
cell_ids = ["C001", "C002", "C003"]
sohs = [0.95, 0.88, 0.79]
for cid, soh in zip(cell_ids, sohs):
print(f"{cid} SOH={soh:.2f}")这正是"特征 + 标签"的雏形:X 一个列表、y 一个列表,下标一一对应。 下标对齐是数据集的命根子 —— 筛选、排序、切分训练集时,X 和 y 必须同进同出。 第 25~26 章会看到 Pandas 怎么用"索引"保证这件事,第 28 章用 groupby 一次性算完所有组。
③ 6.8 节的"别名陷阱"是 AI 代码里最难查的 bug。
X_norm = X_raw
X_norm[:] = (X_norm - mean) / std # ❌ 把原始数据也改了,无法回退!数据预处理管道里,"原始数据只读、处理产生新对象"是一条铁律。 所以工程代码里到处是 df.copy()、x.clone().detach() —— 它们都在解决 6.8 节的问题。
| 本章的坑 | 在 AI / Pandas 里的对应 |
|---|---|
b = a 是别名 | df2 = df1 后改 df2,df1 也变了 |
a.copy() 浅拷贝 | df.copy(deep=True);NumPy 的 arr.copy() |
a[:] 是浅拷贝(新对象) | NumPy 的 arr[:] 是视图,不是拷贝!(第 19 章) |
| 混装类型 | np.array([1, "2"]) 整列变字符串,再也算不了数(第 17 章) |
append 返回 None | df.drop(...) 返回新表;inplace=True 才是原地改 |
一句话总结:列表是"批量样本"的第一个容器。它教会你两件必须带走的事 —— 下标从 0 开始、赋值不是复制。这两条在 NumPy、Pandas、PyTorch 里一个字都没变。