🧮 第2章 变量和数组
📄 一句话概括

数组是一片连号的盒子,一个名字加一个下标就能取到。 下标从 0 开始,是省一次加法:地址 = 首地址 + 下标 × 元素大小。 5 个元素的下标是 0 到 4,写 [5] 必报错——这个错在排序里出现得最多。

📝 本章和前面几章的关系

上一篇一个盒子装一个值。这一篇开始装一排。 教材用「储物柜」打比方:一排柜子,编号从 0 开始。这个比方很好用。 但要补一句:Python 的 list 不是真正的连号内存,这一篇会用 array 演示真连号。

一、一个名字,管一排盒子

scores = [0] * 5 先摆五个空盒子,再一个个放值进去。 取的时候写 scores[0]、scores[4]。scores 是那一排的名字,方括号里是第几个。 演示里五个值是 88、92、75、60、97,取出来对得上。

教材的比方是储物柜:一整排柜子共用一个编号系统,你要第 3 个,就说「第 3 号柜」。 这个比方有个前提容易被忽略:柜子必须是连号的。下一节讲为什么这件事重要。

二、下标越界:最常见的错

五个元素,下标是 0 到 4。 写 scores[5] 不是「第 5 个」,是「第 6 个」——那里没有盒子,报 IndexError。 这个错在排序、查找里出现得最多,因为循环条件写成 i <= len(...) 就必犯。 正确的写法是 i < len(...),或者干脆用 for x in data 不用下标。

记住这句话:长度是 5,最大下标是 4。差一,就是 off-by-one。

三、为什么说数组是连号的

演示里用 array('i', ...) 打印了首地址:五个元素分别在 首地址 + 0、+4、+8、+12、+16。 地址是等差数列,公差就是元素大小(4 字节)。 所以给一个下标 i,算地址只要一次乘法:首地址 + i × 4。这就是「按下标取数很快」的根。

这里要补一句教材没说的: 平时写的 list 存的是一串「对象在哪儿」的地址,本身不连号。教材讲「数组像连号储物柜」, 讲的是 array 这种结构。 这个区别在第 3 章会变成一个具体的取舍: list 取数快是因为它存的地址表连号,不是因为数据本身连号。

四、下标为什么从 0 开始

演示最后那张表:想取第 1 个,下标是 0;第 3 个,下标是 2;第 5 个,下标是 4。 规律是「下标 = 第几个 - 1」。 原因在上一段那个公式里:从 0 开始,第 i 个的地址就是 首地址 + i × 大小,不用先减一。 这是省下来的那一次减法。

所以「从 0 开始」不是谁随口定的,是省一次运算的约定。 第 3 章最后一篇会专门讲为什么有些语言从 1 开始。

五、动手 Demo:一排盒子:放值、取数、越界、看地址

先摆五个空盒子,逐个放值,再按下标取。 然后故意越界一次,看它报什么。 最后用 array 看真连号的内存的地址,验证那个乘法公式。

scores = [0] * 5
for i, v in enumerate([88, 92, 75, 60, 97]):
    scores[i] = v
print(scores, scores[0], scores[4])

scores[5]        # IndexError: list index out of range

import array
arr = array.array('i', [10, 20, 30, 40, 50])
addr, n = arr.buffer_info()
print(addr, addr + arr.itemsize, addr + 2 * arr.itemsize)

实跑输出:

--------------------------------------------------------------------
数组与下标:一片连号的盒子
--------------------------------------------------------------------


  ① 一个名字,管一排盒子
  --------------------------------------------------------------------
  scores = [0] * 5       先摆 5 个空盒子
  逐个放值之后:[88, 92, 75, 60, 97]
  scores[0] = 88   scores[4] = 97
  一共 5 个盒子,下标从 0 数到 4

  ② 下标越界会怎样
  --------------------------------------------------------------------
  scores[5]  ->  IndexError: list index out of range
  5 个盒子的下标是 0 到 4。写 scores[5] 不是「第 5 个」,是「第 6 个」,那里没有盒子,所以报错。
  这个错误在排序、搜索里出现得最多——循环条件写成 i <= len(...) 就必犯。

  ③ 为什么说数组是「连号」的
  --------------------------------------------------------------------
  array('i', [10, 20, 30, 40, 50])
  首地址 2907009571856,每个元素 4 字节,共 5 个
  第 0 个在 2907009571856,第 1 个在 2907009571860,第 2 个在 2907009571864
  地址是等差数列,公差就是元素大小。所以给一个下标 i,算地址只要一次乘法:首地址 + i × 4。
  这就是「按下标取数很快」的根。
  ! 注意:上面用的是 array 模块,它才是真的一片连号内存。
  平时写的 list 存的是一串「对象在哪儿」的地址,本身不连号。
  教材讲「数组像连号储物柜」,讲的是 array 这种结构。

  ④ 下标从 0 开始,是省一次加法
  --------------------------------------------------------------------

| 想取第几个 | 下标 | 地址怎么算 |
|---|---:|---|
| 第 1 个 | 0 | 首地址 + 0×4 |
| 第 3 个 | 2 | 首地址 + 2×4 |
| 第 5 个 | 4 | 首地址 + 4×4 |

  从 0 开始,第 i 个元素的地址就是「首地址 + i × 大小」,不用先减一。
  这是省下来的那一次减法,不是谁随口定的。

①第一段摆盒子用的是 [0] * 5,这是造定长数组最常用的写法。 ②IndexError: list index out of range 这句报错记住,以后一看就知道是下标超了。 ③第三段那三个地址相差正好 4(每个元素 4 字节)。这就是「连号」的证据。 ④注意那句 note:array 才是真连号,list 存的是地址表。教材的比方对应的是前者。 ⑤最后那段「下标 = 第几个 - 1」是这一篇要背下来的唯一一条换算。

30 秒实验

  1. 把 scores[5] 改成 scores[-1],看它取到哪个(Python 允许负数下标)。
  2. 写一个循环,把五个分数全部加 10 分,用下标和不带下标各写一遍。
  3. 把 array('i', ...) 换成 array('l', ...),看地址间隔变成多少。

常见误区

  1. 「五个元素,下标就是 1 到 5。」是 0 到 4。写 [5] 必报错。
  2. 「数组就是连号内存。」list 存的是地址表;array 才是真连号。
  3. 「从 0 开始是为了方便程序员。」是为了省一次减法,让地址能直接乘出来。
自测三问(点开对答案) 1. 五个元素的数组,合法下标是哪些? —— 0 到 4。最大下标 = 长度 - 1。 2. 为什么按下标取数很快? —— 地址是首地址加下标乘元素大小,一次乘法算出来,不用挨个找。 3. 教材说「数组像连号储物柜」,对应 Python 里哪个类型? —— `array`。`list` 存的是对象的地址,本身不连号。

知道数组是什么了。什么时候才该用它?下一节给一条判据,顺带算一下不用数组要多写多少行。

相关笔记