Python 二维列表初始化:知识点总结


一、先讲本质:Python 里“变量存的是引用”

Python 中,列表里的元素本质上保存的是对象的引用(地址),不是“复制出一个全新的对象”。

例如:

a = [1, 2]
b = a
b[0] = 100
print(a)  # [100, 2]

因为 ab 指向的是同一个列表对象


二、核心规律:什么时候共享,什么时候独立


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,但整数不可变,所以没问题。

类似还有:

  • int
  • float
  • str
  • tuple(前提是 tuple 里没有可变成员)
  • None
  • bool

示例

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 个位置都指向同一个空列表。

常见可变对象

  • list
  • dict
  • set
  • 自定义类实例(多数情况下可变)
  • 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] * 5int,不可变可能共享同一个 0安全
['a'] * 5str,不可变可能共享安全
[None] * 5None,不可变共享安全
[[]] * 5list,可变共享危险
[{}] * 5dict,可变共享危险
[set()] * 5set,可变共享危险
[[] 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 不是副本
  • ab 指向同一个对象

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 列表初始化是否共享,关键不在于是不是二维,而在于:你是否在重复使用同一个可变对象的引用。

  • *:重复的是引用,若对象可变,就容易共享出问题
  • 推导式:如果每次都重新创建对象,就彼此独立
  • 不可变对象即使共享也通常安全,可变对象共享则默认危险

十七、给你一个面试/刷题速记版

一维乘法看元素,二维乘法看“行”;
不可变共享无大碍,可变共享必遭殃;
外层初始化用推导,内层元素再细看;
列表字典集合类,想要独立就新建。


更多推荐