CS1602计算导论
第 5 讲Part 2 数据的组织AI Level 0

列表

List

单个变量装不下真实问题。列表是 Python 里最常用的容器,也是新手最容易踩坑的地方——因为它背后是「引用」而不是「值」。

本讲结束后你应当能

  • 用列表存储和处理一批数据,而不是定义一百个变量
  • 说清楚「变量指向对象」这个模型,并用 id() 验证它
  • 分辨「修改一个列表」和「重新赋值一个变量」的区别
  • 避开嵌套列表与浅拷贝的经典陷阱
  • 对常见列表操作的代价有直觉判断
本页目录

从一百个变量说起

假设要统计一个班 116 名学生的成绩:平均分、最高分、有多少人不及格。

用你目前学过的东西,只能这样写:

score1 = 87
score2 = 92
score3 = 65
# …还有 113 个

这不只是麻烦,而是根本走不通——求平均分要把 116 个变量名一个一个写出来相加; 如果下学期变成 120 人,整段代码都要重写。

问题的本质是:变量名是写死在代码里的,而数据的数量是运行时才知道的。 我们需要一种东西,能把一批数据装在一起,用序号而不是名字去访问它们。

这就是列表。

scores = [87, 92, 65, 78, 95]

print("人数:", len(scores))
print("总分:", sum(scores))
print("平均:", sum(scores) / len(scores))
print("最高:", max(scores))

创建与访问

列表用方括号创建,元素之间用逗号隔开。元素可以是任何类型,也可以类型不一致:

a = [1, 3, 9, 7]                 # 四个整数
b = ["Hello", "world"]           # 两个字符串
c = [1, "two", 3.0, True]        # 混合类型,合法
d = []                           # 空列表

print(a, b, c, d)
print(type(a))

也可以用 list() 把其他可迭代的东西转成列表:

print(list("hello"))          # 字符串 → 逐个字符
print(list(range(5)))         # range → 整数序列

下标从 0 开始

访问用方括号加序号。第一个元素的序号是 0,这是初学者最常犯的错。

a = [10, 20, 30, 40, 50]

print(a[0])     # 第一个
print(a[4])     # 第五个,也是最后一个
print(a[-1])    # 负数从右往左数,-1 就是最后一个
print(a[-2])    # 倒数第二个

长度为 n 的列表,合法下标是 0 到 n-1。越界会抛 IndexError:

a = [10, 20, 30]
print(a[3])

遍历

scores = [87, 92, 65, 78, 95]

for s in scores:
    print(s, "及格" if s >= 60 else "不及格")

如果同时需要序号和元素,用 enumerate(),不要写 for i in range(len(scores)):

scores = [87, 92, 65]

for i, s in enumerate(scores):
    print(f"第 {i + 1} 名学生:{s}")

成员判断

in 和 not in 判断某个元素是否在列表里:

names = ["张三", "李四", "王五"]
print("李四" in names)
print("赵六" not in names)

变量指向对象

这一节是本讲的核心。如果只记住一件事,记住这个。

在 Python 里,变量不是一个装东西的盒子,而是一个指向对象的箭头。 赋值语句 x = y 做的事情不是「把 y 的内容复制到 x」,而是「让 x 指向 y 指向的那个对象」。

内置函数 id() 返回一个对象在内存中的身份编号。用它可以把这件事看清楚:

a = [1, 2, 3]
b = a          # b 和 a 指向同一个列表

print("a 的 id:", id(a))
print("b 的 id:", id(b))
print("是同一个对象吗:", a is b)

两个 id() 是一样的——a 和 b 是同一个列表的两个名字。

所以,通过 b 修改列表,a 看到的也变了:

a = [1, 2, 3]
b = a

b.append(4)      # 修改 b 指向的那个列表

print("a =", a)  # a 也变了
print("b =", b)

修改 vs 重新赋值

理解了引用模型,就能分清两种看起来相似、实则完全不同的操作。

重新赋值:让变量指向一个新对象,原对象不受影响。

a = [1, 2, 3]
b = a
b = [9, 9, 9]      # b 改为指向一个新列表

print("a =", a)    # a 没变
print("b =", b)

原地修改:改的是对象本身,所有指向它的名字都看得到。

a = [1, 2, 3]
b = a
b[0] = 9           # 改的是列表内部

print("a =", a)    # a 也变了
print("b =", b)

判断依据很简单:等号左边是光秃秃的变量名,就是重新赋值;带了 [...] 或者调用了方法,就是原地修改。

增删改

列表是可变的(mutable),可以增删改。下面这些方法都是原地修改,返回 None。

方法作用
a.append(x)在末尾追加一个元素
a.extend(it)把另一个可迭代对象的所有元素追加到末尾
a.insert(i, x)在下标 i 处插入 x,原有元素后移
a.remove(x)删除第一个值为 x 的元素,找不到抛 ValueError
a.pop(i)删除并返回下标 i 的元素,i 省略时删最后一个
a.clear()清空
a.sort()原地排序
a.reverse()原地反转
del a[i]删除下标 i 的元素(这是语句,不是方法)
a = [3, 1, 2]

a.append(4)        # [3, 1, 2, 4]
a.insert(0, 0)     # [0, 3, 1, 2, 4]
a.remove(3)        # [0, 1, 2, 4]
last = a.pop()     # 取出 4,a 变成 [0, 1, 2]
a.sort()

print(a, "取出的是", last)

append 和 extend 的区别经常混淆:

a = [1, 2]
a.append([3, 4])
print("append:", a)     # 整个列表作为一个元素塞进去

b = [1, 2]
b.extend([3, 4])
print("extend:", b)     # 把里面的元素逐个追加

切片

切片用来取出列表的一段,语法是 a[start:stop]——含头不含尾。

a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(a[2:5])      # 下标 2、3、4
print(a[:3])       # 从头到下标 2
print(a[7:])       # 从下标 7 到末尾
print(a[:])        # 整个列表(这会得到一个新列表,后面有用)
print(a[-3:])      # 最后三个

第三个参数是步长:

a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

print(a[::2])      # 每隔一个取一个
print(a[1::2])     # 从下标 1 开始,每隔一个
print(a[::-1])     # 步长为负 → 反转(返回新列表,不改原列表)

切片返回的是新列表,这一点很重要:

a = [1, 2, 3]
b = a[:]          # 切片产生新列表

print(a is b)     # False,是两个不同的对象
b[0] = 99
print("a =", a)   # a 不受影响

拷贝的陷阱

上面 a[:] 复制了一份,看起来问题解决了。但它只是浅拷贝(shallow copy)—— 只复制了最外层,内层的对象仍然是共享的。

嵌套列表里这会立刻出事:

a = [[1, 2], [3, 4]]
b = a[:]                # 浅拷贝

b[0][0] = 99            # 改的是内层列表

print("a =", a)         # a 也变了!
print("b =", b)
print("内层是同一个对象吗:", a[0] is b[0])

原因是:b 是新的外层列表,但它的两个元素仍然是指向原来那两个内层列表的箭头。

要真正复制干净,用 copy.deepcopy():

import copy

a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)

b[0][0] = 99
print("a =", a)         # 这次 a 安全了
print("b =", b)
写法复制层数什么时候用
b = a不复制,只是别名你确实想要两个名字指同一个东西
b = a[:] 或 a.copy()只复制最外层列表元素都是数字、字符串等不可变类型
copy.deepcopy(a)递归复制所有层列表里还套着列表、字典等可变对象

用 * 造矩阵的经典事故

想造一个 3×3 的全零矩阵,很多人会这样写:

matrix = [[0] * 3] * 3
print(matrix)              # 看起来完全正确

matrix[0][0] = 1
print(matrix)              # 三行全变了!

[[0] * 3] * 3 的意思是「把 [0,0,0] 这一个列表的引用重复三次」, 所以三行其实是同一个列表。

正确写法是用列表推导,每次循环都真正新建一个列表:

matrix = [[0] * 3 for _ in range(3)]

matrix[0][0] = 1
print(matrix)              # 只有第一行变了

边遍历边删除

另一个高频事故:在 for 循环里删除当前遍历的列表。

a = [1, 2, 2, 3, 4]

for x in a:
    if x == 2:
        a.remove(x)

print(a)      # 结果不是你想要的

为什么?for 内部维护着一个下标,每轮加一。删除元素会让后面的元素整体前移, 于是有些元素被跳过了。

正确做法是不要在原列表上改,而是构造一个新列表:

a = [1, 2, 2, 3, 4]

b = [x for x in a if x != 2]      # 列表推导,最清晰
print(b)

列表推导

上面已经用了几次,这里正式介绍。列表推导是「从一个序列造出另一个序列」的简洁写法:

[表达式 for 变量 in 可迭代对象 if 条件]

它等价于一个循环,但更短、更不容易写错:

# 普通写法
squares = []
for i in range(10):
    squares.append(i ** 2)

# 列表推导
squares2 = [i ** 2 for i in range(10)]

print(squares == squares2)
print(squares2)

带条件的版本:

scores = [87, 92, 45, 78, 55, 95]

passed = [s for s in scores if s >= 60]
print("及格的:", passed)
print("及格率:", len(passed) / len(scores))

操作的代价

最后建立一点直觉。列表在内存里是连续排列的一排格子, 每个格子存着一个指向元素的箭头。这个结构决定了各种操作的快慢:

操作快慢为什么
a[i] 按下标访问很快直接算出格子的位置,一步到位
a.append(x)很快在末尾添一格,不影响其他元素
a.pop() 删最后一个很快同上
a.insert(0, x)慢后面所有元素都要往后挪一格
a.remove(x)慢先要从头找到它,再把后面的往前挪
x in a慢从头挨个比较
a.pop(0) 删第一个慢后面所有元素都要往前挪

这里说的「慢」是指:列表越长,越慢,而且是成正比地慢。 列表有一百万个元素时,a.insert(0, x) 要挪动一百万个位置。

小结

  • 列表用序号访问一批数据,下标从 0 开始,-1 是最后一个
  • 变量是指向对象的箭头。b = a 不复制列表,只是多了个名字
  • 原地修改的方法(append / sort / reverse / extend)一律返回 None
  • 切片 a[i:j] 含头不含尾,返回新列表
  • a[:] 是浅拷贝,嵌套结构要用 copy.deepcopy()
  • [[0] * n] * m 造出来的矩阵所有行是同一个列表,要用列表推导
  • 不要边遍历边修改
  • 末尾增删快,开头增删和查找慢

练习

  1. 不用 max(),自己写一个函数求列表中的最大值。空列表时返回 None。

  2. 写一个函数,去掉列表中的重复元素并保持原有顺序(不能用 set,因为它会打乱顺序)。

  3. 给定一个二维列表(矩阵),写一个函数返回它的转置。先用 [[0] * n] * m 试一次,看看会发生什么,再改对。

  4. 下面这段代码想把列表里所有偶数翻倍,但结果不对。找出原因并改对:

    a = [1, 2, 3, 4, 5, 6]
    for i in range(len(a)):
        if a[i] % 2 == 0:
            a.insert(i, a[i] * 2)
  5. 用 id() 设计一个小实验,验证「切片产生新列表,但内层元素仍是共享的」。

本讲的配套上机题在 Lab 5。