🐍 第3章 列表、循环与自定义函数
📄 一句话概括

一个变量装一个值,装不了「所有的货」。列表就是让一个变量装一串。 下标从 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 秒实验

  1. 把 stock[::-1] 改成 stock[::2],猜一下拿到什么,再跑。
  2. 把演示第四段的 c = a[:] 改成 c = a.copy(),再改成 c = list(a),三次结果应该一样。
  3. 把 nums.sort(reverse=True) 改成 nums = nums.sort(),打印 nums,看它变成什么。

常见误区

  1. 「b = a 就是复制了一份。」它只是多起了一个名字。 id(a) == id(b) 为真,就是同一个东西。 要复制得用 a[:]、a.copy() 或 list(a)。
  2. 「a[1:3] 是第 1 到第 3 个。」是第 2、第 3 个。 切片含头不含尾,这是 Python 的规矩,好处是长度能直接减出来。
  3. 「切片越界会报错。」不会。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 展开讲:它到底能走哪些东西,还有程序库怎么引用。

相关笔记