Python二维列表实测:列表乘法和浅复制为何共享内层对象
改一个位置,为什么三行都变了
用列表存表格、棋盘或批量任务状态时,[[0] * 2] * 3 看起来像“三行两列”。但它创建的是三个指向同一个行列表的引用。下面六组代码已在 Python 3.10.4 中实际运行,并用断言核对结果。
bad = [[0] * 2] * 3
bad[0][0] = 9
print(bad)
# [[9, 0], [9, 0], [9, 0]]
print(bad[0] is bad[1])
# True
问题不在索引:赋值只修改了一个行对象,只是三处都引用它。外层乘法重复的是引用,不会为每一行重新执行一次内部列表的构造。判断是否为同一对象要用 is,不能仅凭两个列表的值相等来判断。
1. 构造时让每行分别创建
good = [[0] * 2 for _ in range(3)]
good[0][0] = 9
print(good)
# [[9, 0], [0, 0], [0, 0]]
print(good[0] is good[1])
# False
列表推导式会在每轮创建新的行列表。这里的单元格是不可变整数,因此可以使用 [0] * 2 初始化一行。如果把单元格换成可变字典或列表,还需要继续检查单元格是否共享,不能把“行已独立”理解为任意层级都独立。
2. 外层copy不会复制每一行
original = [[1, 2], [3, 4]]
copied = original.copy()
copied[0][0] = 8
print(original)
# [[8, 2], [3, 4]]
print(copied is original, copied[0] is original[0])
# False True
copy() 返回新的外层列表,但保留原来的行对象引用。用切片 original[:] 同样是浅复制。只比较外层的身份,容易误以为已经获得完全独立的数据。
对于本例这种只有两层、单元格为整数的结构,可以逐行复制:
original = [[1, 2], [3, 4]]
copied = [row.copy() for row in original]
copied[0][0] = 8
print(original, copied)
# [[1, 2], [3, 4]] [[8, 2], [3, 4]]
这解决了行对象共享;如果行中还嵌套可变对象,逐行浅复制仍然不够,需要根据数据结构决定复制深度。
3. deepcopy也不会自动纠正原有共享关系
from copy import deepcopy
shared = [[0] * 2] * 3
copied = deepcopy(shared)
copied[0][0] = 7
print(shared)
# [[0, 0], [0, 0], [0, 0]]
print(copied)
# [[7, 0], [7, 0], [7, 0]]
这次修改不会影响原对象,但副本中的三行仍指向同一个新行对象。对这个由列表和整数组成的结构,deepcopy 会在一次复制过程中记录已经复制的对象,遇到同一行时复用其副本。因此,“与原对象分离”和“副本内部每行独立”是两个不同要求。
如果确认业务上每行应当独立,可以在本例中分别复制每行:
copied = [deepcopy(row) for row in shared]
copied[0][0] = 7
print(copied)
# [[7, 0], [0, 0], [0, 0]]
每次调用分别建立复制记录,所以这些行会分开。但有些结构本来就需要共享对象,不能不加区分地拆开所有引用。更直接的做法通常是在初始化阶段创建独立行,避免后续修补。
实际排查顺序
先打印修改前后的值,再检查外层、行和可变单元格的身份。初始化表格时逐行创建;复制已有数据时明确需要隔离到哪一层;如果使用 deepcopy,还要确认原数据内部的共享关系是否符合业务设计。六组示例验证的是这些具体结构的行为,不代表所有自定义对象都会按相同方式复制。
- 点赞
- 收藏
- 关注作者
评论(0)