列表
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造出来的矩阵所有行是同一个列表,要用列表推导- 不要边遍历边修改
- 末尾增删快,开头增删和查找慢
练习
-
不用
max(),自己写一个函数求列表中的最大值。空列表时返回None。 -
写一个函数,去掉列表中的重复元素并保持原有顺序(不能用
set,因为它会打乱顺序)。 -
给定一个二维列表(矩阵),写一个函数返回它的转置。先用
[[0] * n] * m试一次,看看会发生什么,再改对。 -
下面这段代码想把列表里所有偶数翻倍,但结果不对。找出原因并改对:
a = [1, 2, 3, 4, 5, 6] for i in range(len(a)): if a[i] % 2 == 0: a.insert(i, a[i] * 2) -
用
id()设计一个小实验,验证「切片产生新列表,但内层元素仍是共享的」。
本讲的配套上机题在 Lab 5。