Python 二维列表初始化:知识点总结
Python 二维列表初始化:知识点总结
一、先讲本质:Python 里“变量存的是引用”
Python 中,列表里的元素本质上保存的是对象的引用(地址),不是“复制出一个全新的对象”。
例如:
a = [1, 2]
b = a
b[0] = 100
print(a) # [100, 2]
因为 a 和 b 指向的是同一个列表对象。
二、核心规律:什么时候共享,什么时候独立
1. 使用 * 重复列表时:通常是“复制引用”
例子 1:一维列表里放不可变对象
arr = [0] * 5
print(arr) # [0, 0, 0, 0, 0]
这里看起来没问题,因为 0 是 不可变对象,即使多个位置都引用同一个 0,也没风险。
2. 二维列表里如果写成 [[0]*m] * n:外层共享
mat = [[0] * 3] * 4
看似得到 4 行 3 列矩阵,实际上:
[[0]*3]先生成 一个内层列表- 再通过
* 4把这个同一个内层列表的引用重复 4 次
所以 4 行其实是同一个对象。
验证:
mat = [[0] * 3] * 4
print(id(mat[0]), id(mat[1]), id(mat[2]), id(mat[3]))
# 全一样
修改一行:
mat[0][0] = 9
print(mat)
# [[9, 0, 0], [9, 0, 0], [9, 0, 0], [9, 0, 0]]
结论
[[0]*m] * n中,外层是共享的- 所有行指向同一个内层列表
3. 使用列表推导式时:每次表达式重新执行,通常独立
mat = [[0] * 3 for _ in range(4)]
这里每次循环都会重新创建一个 [0] * 3,所以每一行是独立的。
验证:
print(id(mat[0]), id(mat[1]), id(mat[2]), id(mat[3]))
# 不同
修改:
mat[0][0] = 9
print(mat)
# [[9, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 0]]
结论
[expr for _ in range(n)]- 如果
expr每次会创建新对象,那么结果就是独立对象
三、最重要的判断标准:看“被重复的对象”是不是可变对象
这是核心中的核心。
1. 如果重复的是不可变对象:共享通常无害
例如:
arr = [0] * 5
这里 5 个位置可能都引用同一个整数对象 0,但整数不可变,所以没问题。
类似还有:
intfloatstrtuple(前提是 tuple 里没有可变成员)Nonebool
示例
a = ['x'] * 3
a[0] = 'y'
print(a) # ['y', 'x', 'x']
这里不是修改原字符串,而是让 a[0] 指向新字符串 'y',不影响其他位置。
结论
- 重复不可变对象,通常安全
- 但本质上仍可能是“共享引用”,只是因为不可变所以不会出问题
2. 如果重复的是可变对象:共享通常危险
例如:
arr = [[]] * 3
arr[0].append(1)
print(arr) # [[1], [1], [1]]
因为 3 个位置都指向同一个空列表。
常见可变对象
listdictset- 自定义类实例(多数情况下可变)
bytearray
结论
- 重复可变对象,默认危险
- 特别是嵌套列表中非常容易踩坑
四、二维列表里,到底什么时候共享,什么时候独立
情况 1:[[0]*m] * n
mat = [[0] * 3] * 4
结构分析
- 内层
[0]*3:生成一个独立列表对象 - 外层
*4:复制这个列表的引用 4 次
结果
- 行与行之间:共享
- 行内元素
0:不可变,共享无害
总结
- 外层共享
- 内层元素是不可变对象,所以单元值本身没问题
- 一旦你修改某一行的某一列,本质是在改共享的那一行对象,所以所有行都变
情况 2:[[0]*m for _ in range(n)]
mat = [[0] * 3 for _ in range(4)]
结构分析
- 每次循环都新建一个
[0]*3
结果
- 每一行:独立
- 行内的
0:不可变,共享无害
总结
- 外层独立
- 每行独立
- 这是二维数值矩阵初始化的标准写法
情况 3:[[]] * n
arr = [[]] * 3
结果
- 所有元素都指向同一个空列表
总结
- 共享
- 高危
情况 4:[[] for _ in range(n)]
arr = [[] for _ in range(3)]
结果
- 每个空列表都新建
- 相互独立
总结
- 独立
- 推荐
情况 5:[[{}]*m for _ in range(n)]
这个是很多人会忽略的坑。
mat = [[{}] * 3 for _ in range(2)]
分析
- 外层推导式让每一行独立
- 但每一行中的 3 个
{}是同一个字典引用被重复了
验证:
mat[0][0]['a'] = 1
print(mat)
# [[{'a': 1}, {'a': 1}, {'a': 1}], [{}, {}, {}]]
结论
- 行与行之间:独立
- 同一行内的元素:共享
正确写法
mat = [[{} for _ in range(3)] for _ in range(2)]
五、一个非常实用的总规则
规则 1:* 不会递归复制对象,只会重复引用
[x] * n
等价理解为:
- 把 同一个对象 x
- 放进列表里 n 次
如果 x 可变,就会共享;
如果 x 不可变,共享通常无害。
规则 2:推导式会多次执行表达式
[expr for _ in range(n)]
如果 expr 每次产生新对象,那么结果独立。
比如:
[[] for _ in range(3)] # 每次都是新列表
[{} for _ in range(3)] # 每次都是新字典
[[0]*2 for _ in range(3)] # 每次都是新行
六、共享和独立的判断表
1. 一维情况
| 写法 | 元素类型 | 是否共享 | 是否安全 |
|---|---|---|---|
[0] * 5 | int,不可变 | 可能共享同一个 0 | 安全 |
['a'] * 5 | str,不可变 | 可能共享 | 安全 |
[None] * 5 | None,不可变 | 共享 | 安全 |
[[]] * 5 | list,可变 | 共享 | 危险 |
[{}] * 5 | dict,可变 | 共享 | 危险 |
[set()] * 5 | set,可变 | 共享 | 危险 |
[[] for _ in range(5)] | list,可变 | 独立 | 安全 |
[{} for _ in range(5)] | dict,可变 | 独立 | 安全 |
2. 二维情况
| 写法 | 行之间 | 行内元素之间 | 是否安全 |
|---|---|---|---|
[[0]*m] * n | 共享 | 0 不可变 | 危险(行共享) |
[[0]*m for _ in range(n)] | 独立 | 0 不可变 | 安全 |
[[[]]*m for _ in range(n)] | 行独立 | 同一行内共享 | 危险 |
[[[] for _ in range(m)] for _ in range(n)] | 独立 | 独立 | 安全 |
[[{}]*m] * n | 行共享 | 字典也共享 | 非常危险 |
[[{} for _ in range(m)] for _ in range(n)] | 独立 | 独立 | 安全 |
七、很多人误解的一点:[0]*m 为什么安全,而 [[0]*m]*n 危险?
这是面试里经常问的点。
[0] * m 安全的原因
因为这里重复的是整数 0,整数不可变。
row = [0] * 3
row[0] = 1
print(row) # [1, 0, 0]
你不是在“修改 0”,而是让 row[0] 指向了新的整数 1。
[[0]*m] * n 危险的原因
危险的不是里面的 0,而是行列表本身被共享了。
mat = [[0]*3] * 2
mat[0][0] = 1
这里相当于:
mat[0]和mat[1]是同一个列表- 修改
mat[0][0]就是在修改那个共享的行对象
八、进一步延伸:多维列表怎么判断?
错误示例
cube = [[[0] * 2] * 3] * 4
这会导致多层共享,非常混乱。
结论
- 维度越高,用
*初始化嵌套可变对象越危险 - 多维结构建议全部用推导式递归创建
正确写法
cube = [[[0 for _ in range(2)] for _ in range(3)] for _ in range(4)]
或者:
cube = [[[0] * 2 for _ in range(3)] for _ in range(4)]
因为最内层是不可变值 0,所以这样也安全。
九、和浅拷贝、深拷贝的关系
这块你原文提到了,但可以再补全。
1. 赋值不是拷贝
a = [[1], [2]]
b = a
b不是副本a和b指向同一个对象
2. 浅拷贝:只复制最外层,内层仍共享
import copy
a = [[1], [2]]
b = copy.copy(a)
b[0].append(9)
print(a) # [[1, 9], [2]]
原因
- 外层列表新建了
- 但里面的子列表还是同一个引用
同理:
b = a[:]
b = list(a)
也都是浅拷贝。
3. 深拷贝:递归复制所有层
import copy
a = [[1], [2]]
b = copy.deepcopy(a)
b[0].append(9)
print(a) # [[1], [2]]
结论
- 多层嵌套且需要完全独立时,使用
deepcopy - 但性能较差,不适合大规模频繁操作
十、实战里最常见的场景总结
1. 动态规划 DP 表
错误
dp = [[0] * m] * n
正确
dp = [[0] * m for _ in range(n)]
2. 图的邻接表
错误
graph = [[]] * n
正确
graph = [[] for _ in range(n)]
否则给一个点加边,所有点都加了。
3. 棋盘 / 地图 / visited 数组
正确
visited = [[False] * cols for _ in range(rows)]
board = [['.'] * cols for _ in range(rows)]
因为:
- 每一行独立
False和'.'是不可变对象
4. 每个格子里都要放列表/字典
错误
cells = [[[]] * cols for _ in range(rows)]
正确
cells = [[[] for _ in range(cols)] for _ in range(rows)]
或
cells = [[{} for _ in range(cols)] for _ in range(rows)]
十一、如何快速判断某个初始化写法是否会共享?
方法 1:看有没有 * 作用在可变对象上
危险信号
[[]] * n
[{}] * n
[[0]*m] * n
[[[]]*m for _ in range(n)]
凡是 * 左边是可变对象,就要警惕。
方法 2:用 id() 检查
mat = [[0] * 2] * 3
print(id(mat[0]), id(mat[1]), id(mat[2]))
若相同,说明共享。
检查某一行内部元素:
mat = [[{}] * 3 for _ in range(2)]
print(id(mat[0][0]), id(mat[0][1]), id(mat[0][2]))
若相同,说明同一行内共享字典。
方法 3:试改一个位置,看别的地方会不会联动
obj[0][0] = 'X'
观察其他位置是否变化。
十二、一个更准确的“安全原则”
你原文里说:
[x] * n安全条件:x 为不可变对象 且 后续会被覆盖
这个说法可以再精确一点。
更准确版本
[x] * n 什么时候安全?
安全情况 A:x 是不可变对象
例如:
[0] * 5
[''] * 5
[None] * 5
安全。
安全情况 B:x 虽然可变,但你不会原地修改它
理论上可以,但不推荐,因为容易误用。
例如:
arr = [[]] * 3
arr[0] = [1, 2]
print(arr) # [[1, 2], [], []]
这里你是给 arr[0] 重新绑定一个新列表,没有修改共享的那个空列表,所以暂时没出错。
但一旦这样:
arr[1].append(9)
就炸了。
结论
- 工程实践里:只要
x可变,就默认[x] * n不安全 - 不要依赖“我以后不修改它”这种假设
十三、顺带补充一个相关高频坑:函数默认参数共享
这和二维列表共享本质一样,都是“可变对象被复用”。
错误示例
def f(x, arr=[]):
arr.append(x)
return arr
print(f(1)) # [1]
print(f(2)) # [1, 2]
print(f(3)) # [1, 2, 3]
因为默认参数 arr=[] 只在函数定义时创建一次,后面一直复用。
正确写法
def f(x, arr=None):
if arr is None:
arr = []
arr.append(x)
return arr
关联结论
- 共享可变对象陷阱不仅出现在列表乘法里
- 也出现在默认参数、缓存对象、类属性等地方
十四、推荐的标准写法模板
1. 二维数值矩阵
mat = [[0] * cols for _ in range(rows)]
2. 邻接表
graph = [[] for _ in range(n)]
3. 二维字典格子
grid = [[{} for _ in range(cols)] for _ in range(rows)]
4. 三维数组
dp = [[[0] * k for _ in range(m)] for _ in range(n)]
十五、最终记忆版:什么时候共享,什么时候独立
会共享的情况
1. 赋值
b = a
2. * 重复可变对象
[[]] * n
[{}] * n
[[0]*m] * n # 共享的是行列表
3. 浅拷贝嵌套结构时,内层共享
copy.copy(a)
a[:]
list(a)
4. 默认参数使用可变对象
def f(x, arr=[]): ...
会独立的情况
1. 推导式中每次新建对象
[[] for _ in range(n)]
[[0]*m for _ in range(n)]
[[{} for _ in range(m)] for _ in range(n)]
2. 深拷贝
copy.deepcopy(a)
3. 手动逐层创建新对象
new = [sub[:] for sub in old] # 适合二维、内层是一维简单元素
十六、一句最完整的总结
Python 列表初始化是否共享,关键不在于是不是二维,而在于:你是否在重复使用同一个可变对象的引用。
*:重复的是引用,若对象可变,就容易共享出问题- 推导式:如果每次都重新创建对象,就彼此独立
- 不可变对象即使共享也通常安全,可变对象共享则默认危险
十七、给你一个面试/刷题速记版
一维乘法看元素,二维乘法看“行”;
不可变共享无大碍,可变共享必遭殃;
外层初始化用推导,内层元素再细看;
列表字典集合类,想要独立就新建。
更多推荐
所有评论(0)