一个变量装一个值,装不了「所有的货」。列表就是让一个变量装一串。
下标从 0 开始,切片含头不含尾;越界时取值立刻报错,切片只给到能给的为止。
最该记的一条:b = a 不是复制,是多起了一个名字。改 b 就是改 a。
第 2 章的测验是五道题一条条写下来的,那时候还没法「一次处理很多笔」。 教材第 3 章开头两节是【保存多笔相关数据的列表】和【使用循环运行重复作业】。 这两节其实是同一件事的两面:列表负责「装」,循环负责「走一遍」。 本篇先把列表本身说透,循环只讲到「能走一遍」;下一篇再把 for 展开讲。
一、为什么需要列表
卖五种货,你要存五个名字。写成 s1 = '螺丝'、s2 = '扳手'… 到第 50 种就没法看了。
列表解决的就是这件事:一个变量装一串,位置从 0 数。
stock = ['螺丝', '扳手', '电钻', '卷尺', '水平仪'],len(stock) 是 5,stock[0] 是螺丝,
stock[-1] 是水平仪。
负下标是「从尾巴往回数」的简写:-1 是最后一个,-2 是倒数第二个。
它比 len(stock) - 1 短,而且不用先知道长度。
列表里可以放不同类型的东西——数字、字符串、甚至另一个列表。
这是 Python 的宽容,也是它容易出脏数据的原因。真要存结构一致的一批记录,后面第 7 章会用字典装。
二、切片:含头不含尾
stock[1:3] 给出的是第 2、第 3 个,不是第 2 到第 4 个。
这是 Python 最反直觉的一条规矩,但它是故意的。
含头不含尾的好处:stock[1:3] 的长度是 3 - 1 = 2,一眼能算。
stock[:3] 是前三个,stock[3:] 是从第 4 个到尾巴,两头都不用写 len。
stock[::-1] 是整串倒过来。那个 -1 是步长,负数表示倒着走。
切片拿到的永远是一个新列表。这一点跟 b = a 完全不一样,后面会用到。
三、越界:取值会炸,切片不会
stock[9] 只有 5 个元素,立刻抛 IndexError: list index out of range,退出码非 0。
但 stock[2:99] 不报错,它老老实实给你 ['电钻', '卷尺', '水平仪'],给到没有为止。
同一件事,两种行为。 单点取值是「我就要这一个」,拿不到就是错;
切片是「给我这一段」,短一点也认。
写循环时要记住这条。用 for x in items 不会越界;用 for i in range(len(items)) 也不会;
但如果你手动推下标,多推一格就是 IndexError。
四、加、删、改
| 操作 | 作用 | 注意 |
|---|---|---|
append(x) |
加到尾巴 | 最常用,一次一个 |
insert(i, x) |
插到第 i 位 | 后面全部往后挪,慢 |
remove(x) |
按值删 | 找不到就报 ValueError |
pop() |
拿出尾巴那个 | 它把值还给你了 |
a[i] = x |
改第 i 个 | 下标必须已存在 |
remove 找不到值会报错,不会安静跳过。
这一条重要:「什么都没发生」和「报错了」是两种完全不同的信号,别指望它默默帮你。
pop() 不带参数拿最后一个,带参数 pop(0) 拿第一个。
它跟 remove 的区别是:remove 按值找,pop 按位置拿。
改第 i 个用 a[i] = x,但下标必须已经存在。想往第 9 位写而列表只有 5 个,照样 IndexError。
列表不会自动补空位。
五、改名不等于复制(本篇最重要的一条)
演示第四段是这么写的:a = [1, 2, 3],b = a,然后 b.append(4)。
结果 a 也变成了 [1, 2, 3, 4]。id(a) 和 id(b) 打出来完全一样。
b = a 只是给同一个列表多起了一个名字,没有复制任何东西。 变量是贴在数据上的标签,不是箱子。
要真的复制一份,有三条路:a[:](切片)、list(a)、a.copy()。
三条等价,随便挑一条,习惯上 a.copy() 最直白。
这个坑最常出没在「我想留一份原始数据做对比」的时候。演示里 c = a[:] 之后改 c,a 就不动了。
顺带记住:数字、字符串、元组没有这个问题。b = a 之后改 b,a 不变——因为它们不给改。
第 4 章会把这条展开讲,名字叫「可变与不可变」。
六、排序:sorted 和 sort 差在哪
sorted(a) 返回一个新列表,原表不动;a.sort() 直接改原表,不给返回值。
演示里两个都跑了:sorted 之后原表还是 [38, 12, 95, 7, 64],
sort() 之后原表变成 [95, 64, 38, 12, 7]。
要保留原始顺序就用 sorted。 这一条在「先按数量排一遍看看,再按名字排一遍」的时候特别有用。
还有一个常见困惑:a = a.sort() 之后 a 变成 None。因为 sort() 不给返回值,它只负责改。
想接住结果就用 sorted。
七、循环:把列表走一遍
列表建好之后,最常做的事是「每一个都处理一次」。这就是 for。
for name, qty in items: 这一行有个细节:每个元素是两个值的元组,可以直接拆成两个变量,
不用写 items[i][0]。
这个写法叫解包,只要右边是「个数对得上的序列」就能用。列表里装元组、装列表都行。
演示最后打了一张小表,末尾还有一行合计。合计怎么来的?
循环外面先设 total = 0,每轮 total += qty。这个套路后面每一章都会用到。
八、动手 Demo:列表:一个变量装一串东西
先建一个五种货的列表,把下标、负下标、切片、倒序都试一遍。
再故意越界,看取值和切片两种行为差在哪。
然后是最容易踩的那条:b = a 到底复制了没有。
最后用循环打一张带合计的小表。
print('① 建一个列表,按位置取')
stock = ['螺丝', '扳手', '电钻', '卷尺', '水平仪']
print(' stock =', stock)
print(' len(stock) =', len(stock))
print(' stock[0] =', stock[0])
print(' stock[-1] =', stock[-1])
print(' stock[1:3] =', stock[1:3])
print(' stock[::-1] =', stock[::-1])
print('② 越界会报什么')
try:
stock[9]
except IndexError as e:
print(' stock[9] → IndexError:', e)
print(' 切片越界不报错:stock[2:99] =', stock[2:99])
print('③ 加、删、改')
stock.append('锤子')
print(' append(\'锤子\') →', stock)
stock.insert(0, '手套')
print(' insert(0, \'手套\') →', stock)
stock.remove('卷尺')
print(' remove(\'卷尺\') →', stock)
got = stock.pop()
print(' pop() 拿出 %s →' % got, stock)
stock[0] = '绝缘手套'
print(' 改第 0 个 →', stock)
try:
stock.remove('不存在的东西')
except ValueError as e:
print(' remove 找不到 →', type(e).__name__)
print('④ 改名不等于复制')
a = [1, 2, 3]
b = a
b.append(4)
print(' a =', a, ' b =', b)
print(' id(a)==id(b) →', id(a) == id(b))
c = a[:]
c.append(5)
print(' c = a[:] 之后 c.append(5) → a =', a, ' c =', c)
print('⑤ 排序、求和、找位置')
nums = [38, 12, 95, 7, 64]
print(' 原始 ', nums)
print(' sorted ', sorted(nums), ' ← 返回新列表,原表不动')
print(' 原表还是', nums)
print(' sum ', sum(nums))
print(' max/min ', max(nums), '/', min(nums))
print(' index(95)', nums.index(95))
nums.sort(reverse=True)
print(' sort 之后', nums, ' ← 直接改原表')
print('⑥ 循环里用列表')
items = [('螺丝', 3), ('扳手', 12), ('电钻', 1)]
print(' 品名 数量')
total = 0
for name, qty in items:
print(' %-8s %5d' % (name, qty))
total += qty
print(' %-8s %5d' % ('合计', total))
实跑输出:
--------------------------------------------------------------------
列表:一个变量装一串东西
--------------------------------------------------------------------
① 建一个列表,按位置取
--------------------------------------------------------------------
stock = ['螺丝', '扳手', '电钻', '卷尺', '水平仪']
len(stock) = 5
stock[0] = 螺丝
stock[-1] = 水平仪
stock[1:3] = ['扳手', '电钻']
stock[::-1] = ['水平仪', '卷尺', '电钻', '扳手', '螺丝']
下标从 0 开始,-1 是最后一个。切片是「含头不含尾」:[1:3] 给的是第 2、3 个,不是第 2 到第 4 个。
② 越界会报什么
--------------------------------------------------------------------
stock[9] → IndexError: list index out of range
切片越界不报错:stock[2:99] = ['电钻', '卷尺', '水平仪']
单点取值越界立刻报错,切片越界只给到能给的为止。这两种行为不一样,写循环时要记住。
③ 加、删、改
--------------------------------------------------------------------
append('锤子') → ['螺丝', '扳手', '电钻', '卷尺', '水平仪', '锤子']
insert(0, '手套') → ['手套', '螺丝', '扳手', '电钻', '卷尺', '水平仪', '锤子']
remove('卷尺') → ['手套', '螺丝', '扳手', '电钻', '水平仪', '锤子']
pop() 拿出 锤子 → ['手套', '螺丝', '扳手', '电钻', '水平仪']
改第 0 个 → ['绝缘手套', '螺丝', '扳手', '电钻', '水平仪']
append 加在尾巴,insert 插到指定位置,remove 按值删,pop 按位置拿出来。
remove 找不到值会报 ValueError,不是安静跳过。
④ 改名不等于复制
--------------------------------------------------------------------
a = [1, 2, 3]
b = a 两个名字指向同一个列表
b.append(4)
现在 a = [1, 2, 3, 4] b = [1, 2, 3, 4]
id(a)==id(b) → True
c = a[:] 这才是复制
c.append(5) → a = [1, 2, 3, 4] c = [1, 2, 3, 4, 5]
这一条是列表最容易踩的坑:b = a 只是多了一个名字,改 b 就是改 a。
要真的复制一份,用切片 a[:] 或 list(a) 或 a.copy()。
⑤ 排序、求和、找位置
--------------------------------------------------------------------
原始 [38, 12, 95, 7, 64]
sorted [7, 12, 38, 64, 95] ← 返回新列表,原表不动
sum 216
max/min 95 / 7
index(95) 2
sort 之后 [95, 64, 38, 12, 7] ← 直接改原表
sorted() 给你一个新列表,.sort() 就地改。想保留原始顺序就用 sorted。
⑥ 循环里用列表
--------------------------------------------------------------------
品名 数量
螺丝 3
扳手 12
电钻 1
合计 16
for name, qty in items 里,每个元素是两个值的元组,直接拆成两个变量,不用写 items[i][0]。
第一段那六行把切片的规矩全摆出来了:含头不含尾,所以 [1:3] 只有两个元素;负下标从尾巴数; [::-1] 的 -1 是步长,表示倒着走。 第二段是两种越界行为的对照。取值越界立刻报错,切片越界只给到能给的为止。 这个差别在写分页、写批量处理时会反复碰到。 第三段末尾那句是重点:remove 找不到值会抛 ValueError,不是安静跳过。 程序里「什么都没发生」和「报错了」是两种完全不同的信号。 第四段是全篇最该记住的。b = a 之后 id 相同,说明它们是同一个列表。改 b 就是改 a。 要复制得写 a[:] 或者 a.copy()。 第五段两个排序的结果不一样:sorted 给了新列表,原表没动;sort 直接把原表改了。 顺带记住 a = a.sort() 会让 a 变成 None。 第六段那个 for name, qty in items 是解包写法。每个元素是两元的元组,直接拆成两个变量,不用写下标。
30 秒实验
- 把
stock[::-1]改成stock[::2],猜一下拿到什么,再跑。 - 把演示第四段的
c = a[:]改成c = a.copy(),再改成c = list(a),三次结果应该一样。 - 把
nums.sort(reverse=True)改成nums = nums.sort(),打印nums,看它变成什么。
常见误区
- 「
b = a就是复制了一份。」它只是多起了一个名字。id(a) == id(b)为真,就是同一个东西。 要复制得用a[:]、a.copy()或list(a)。 - 「
a[1:3]是第 1 到第 3 个。」是第 2、第 3 个。 切片含头不含尾,这是 Python 的规矩,好处是长度能直接减出来。 - 「切片越界会报错。」不会。
a[2:99]照样给你东西,给到没有为止。 只有单点取值a[9]才报IndexError。
自测三问(点开对答案)
1. `a = [1, 2, 3]`,`b = a`,`b.append(4)` 之后 `a` 是几?为什么? —— `a` 是 `[1, 2, 3, 4]`。因为 `b = a` 没有复制列表,只是让 `b` 和 `a` 指向同一个列表。 `id(a) == id(b)` 为真就是证据。要复制得写 `b = a.copy()`。 2. `stock[9]` 报错,`stock[2:99]` 却不报错,这个差别是怎么来的? —— 两条路的语义不同。单点取值是「我就要这一个」,拿不到就是错,所以抛 `IndexError`; 切片是「给我这一段」,短一点也认,所以给到能给的为止。 3. `sorted(a)` 和 `a.sort()` 有什么不一样? —— `sorted(a)` 返回一个新列表,原表一个字节都不动;`a.sort()` 直接改原表,返回值是 `None`。 所以要保留原始顺序就用 `sorted`,写 `a = a.sort()` 会把 `a` 变成 `None`。下一篇把 for 展开讲:它到底能走哪些东西,还有程序库怎么引用。
- ☐ (本篇完,下一篇 3.2 for in 循环与引用程序库)