我用乘法快速初始化了一个二维列表,给其中一个格子赋值,结果发现每一行的那个格子都跟着变了:一次 Python 列表乘法复制的是引用而非副本的深度复盘

我要初始化一个 3×3 的二维列表,图省事用了乘法 grid =

我用乘法快速初始化了一个二维列表,给其中一个格子赋值,结果发现每一行的那个格子都跟着变了:一次 Python 列表乘法复制的是引用而非副本的深度复盘

那个 bug 是计分板"改一个格子、整列都变"才暴露的:我要初始化一个 3×3 的二维列表(比如棋盘/计分矩阵),图省事用了乘法:grid = [[0] * 3] * 3。打印出来 [[0,0,0],[0,0,0],[0,0,0]],看着完全正确。可当我给某个格子赋值 grid[0][0] = 1 时,诡异的事发生了:打印 grid,变成了 [[1,0,0],[1,0,0],[1,0,0]]——我明明只改了第 0 行第 0 列,可每一行的第 0 列都变成了 1!我盯着这个"一改全改"愣住了。我把这几个列表的 id() 打出来一对比,才看明白,后背发凉:问题出在 [[0]*3] * 3 这个乘法上。列表的 * 3 操作,复制的是"引用"而不是"副本"——它把同一个内层列表对象 [0,0,0] 的引用,重复放了 3 次;也就是说,grid 里的三个"",根本不是三个独立的列表,而是指向同一个列表对象的三个引用;于是我 grid[0][0] = 1 修改的那个列表,同时就是 grid[1]、grid[2]——它们仨是同一个,所以"每一行"的第 0 列都变了(因为它们本就是一行)。根本原因是:列表的乘法(以及很多"复制"操作)对于其中的可变对象,复制的是引用(浅层),而非独立的副本;于是"多份"其实是"同一份的多个引用",改一个就改了全部。问题的根,是 [[0]*3]*3 的乘法复制的是内层列表的引用(浅复制),三行指向同一个列表对象,改一行即改全部。这篇就把这次"列表乘法复制引用"的坑,从头到尾复盘一遍。

故障现场:改一个格子,整列都变

问题在于 [[0]*3]*3 把同一个内层列表的引用重复了 3 次:

# ✗ 出问题的代码: 用乘法初始化二维列表
grid = [[0] * 3] * 3
print(grid)            # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]  ← 看着完全正确

grid[0][0] = 1         # 只想改第0行第0列
print(grid)            # ✗ [[1, 0, 0], [1, 0, 0], [1, 0, 0]]  ← 每一行的第0列都变了!

# 验证: 三个"行"其实是同一个对象
print(id(grid[0]) == id(grid[1]) == id(grid[2]))   # ✗ True! 三行是同一个列表对象!

# 为什么? 列表的 * 复制的是"引用", 不是"独立副本":
# 1. [0] * 3 → [0,0,0] (这层没问题, int是不可变的);
# 2. [[0,0,0]] * 3 → 把【同一个内层列表[0,0,0]的引用】重复放3次;
#    → grid = [行, 行, 行], 而这三个"行"是【指向同一个列表对象】的三个引用!
# 3. grid[0][0] = 1: 修改的是那个唯一的内层列表;
#    → 由于grid[0]、grid[1]、grid[2]都指向它, 所以"三行"的第0列全变了。

# 同样的坑也出现在:
#   row = [0] * 3
#   grid = [row] * 3        # 3个引用都指向同一个row
#   # 或用 [x] * n 创建"n个可变对象"时(其实是n个同一对象的引用)

# 对比: 为什么 [0] * 3 没事? 因为里面是int(不可变), 你"改"它其实是替换成新int, 不影响别人;
#   而内层是list(可变), 多个引用指向它, 改它的内容, 所有引用都看得见。

# 关键: 列表乘法 [x] * n 对可变对象x复制的是【引用】(n个引用指向同一个x), 不是n个独立副本;
#       改其中"一个"就改了全部 —— 用乘法创建"多个可变对象"是陷阱。

第一次看到 id(grid[0]) == id(grid[1])True 时,我又荒谬又恍然:"我以为 *3 是'复制出三个一样的列表',完全没想到它是'把同一个列表摆了三次';它们看起来是三行,其实是一行被引用了三次。"这个坑最隐蔽的地方在于:打印出来完全正确([[0,0,0],[0,0,0],[0,0,0]] 看着就是个正常的二维列表),给了你"初始化成功了"的错觉;只有当你去"修改其中一个内层列表"时,"它们是同一个"的真相才暴露;而且如果你只是"整行替换"(grid[0] = [...])而非"改行内的元素",又碰巧没事(替换的是引用),更迷惑下面就来拆解,Python 的复制到底是怎么回事、二维列表该怎么建。

第一件事:搞懂引用、浅复制与"复制的是什么"

我顺着这次事故,把 Python 的引用、复制层次彻底理清了。

Python 的 [x]*n、复制, 到底复制的是什么?

【核心: Python变量是引用; [x]*n / 浅复制 复制的是"引用"而非可变对象的独立副本; 多个引用指向同一个可变对象, 改一个即改全部】

1. Python 变量是"引用": 变量存的是"指向对象的引用", 不是对象本身
   - a = [1,2]; b = a → a和b指向【同一个】列表; b.append(3) 后 a也变了(它俩是一个)。

2. [x] * n 复制的是"引用":
   - [x] * n → 得到n个【指向同一个x】的引用(不是n个x的独立副本);
   - 若x是不可变的(int/str/tuple): "改"它其实是替换成新对象, 不影响别的引用 → 没事;
   - 若x是可变的(list/dict/set): 改它的内容, 所有指向它的引用都看得见 → 一改全改(本文)。

3. 复制的几个层次:
   - 引用复制(b = a): 完全是同一个对象, 改一个全改;
   - 浅复制(list(a) / a[:] / a.copy() / [x]*n): 复制最外层, 但内层的可变对象仍是【共享引用】;
     → 改外层结构不影响原对象, 但改内层可变对象会互相影响;
   - 深复制(copy.deepcopy(a)): 递归复制所有层, 完全独立, 改谁都不影响谁。

4. 本文的坑就是"浅"的问题:
   - [[0]*3] * 3: 外层乘法复制了"行"的引用, 但这些引用都指向【同一个内层列表】(浅);
   - → 改内层列表(grid[0][0]=1), 所有"行"都受影响。

5. 正确创建"n个独立可变对象":
   - 用列表推导(每次循环新建一个): [[0]*3 for _ in range(3)] → 3个独立的内层列表;
   - 推导式里的表达式每次迭代都【重新求值】, 生成全新的对象 → 不共享。

6. 同源的坑: 可变默认参数(同350篇)、dict.fromkeys(keys, [])(值共享同一list)等, 都是"多处共享同一可变对象"。

一句话: Python变量是引用; [x]*n和浅复制复制的是引用、内层可变对象仍共享; 多个引用指向同一个可变对象时
   改一个即改全部; 要n个独立的可变对象用列表推导[... for _ in range(n)], 要完全独立用deepcopy。

这套认知,是整个坑的根。Python 变量是引用——变量存的是指向对象的引用,b = a 后 a、b 指向同一个列表,改一个全改[x] * n 复制的是引用——得到 n 个指向同一个 x 的引用;x 不可变(int/str)时""是替换新对象不影响别人,x 可变(list/dict)时改它的内容所有引用都看得见、一改全改复制的几个层次:引用复制(同一对象)、浅复制(复制外层、内层可变对象仍共享引用)、深复制(deepcopy 递归完全独立)本文是浅的问题:[[0]*3]*3 外层复制了行的引用、都指向同一个内层列表。正确创建 n 个独立可变对象:用列表推导 [[0]*3 for _ in range(3)](每次迭代重新求值、生成全新对象、不共享)。同源的坑:可变默认参数、dict.fromkeys(keys, []) 等。一句话:Python 变量是引用;[x]*n 和浅复制复制的是引用、内层可变对象仍共享;多个引用指向同一个可变对象时改一个即改全部;要 n 个独立的可变对象用列表推导,要完全独立用 deepcopy。

第二件事:正解——用列表推导创建独立的内层列表

搞懂了原理,正解就清晰了:要创建"n 个独立的可变对象"(二维列表的各行),用列表推导(每次迭代新建一个),而不是用乘法(复制引用);需要完全独立的副本用 copy.deepcopy

# ====== 正解一: 用列表推导创建二维列表(每行独立) ======
grid = [[0] * 3 for _ in range(3)]   # ★ 推导式每次迭代都新建一个 [0,0,0]
print(grid)            # [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
grid[0][0] = 1
print(grid)            # ✓ [[1, 0, 0], [0, 0, 0], [0, 0, 0]]  只改了第0行!

print(id(grid[0]) == id(grid[1]))   # ✓ False! 三行是独立的不同对象
# → 关键: [... for _ in range(3)] 里的 [0]*3 每次循环都【重新求值】, 生成全新的列表 → 三行互相独立。
# (注意: 内层的 [0]*3 没问题, 因为元素是int不可变; 只有"创建多个可变对象"时才需要推导式。)

# ====== 正解二: 需要复制一个已有的嵌套结构 → 按需选浅/深复制 ======
import copy
original = [[1, 2], [3, 4]]
shallow = original[:]              # 浅复制: 外层新列表, 但内层[1,2]/[3,4]仍共享
shallow[0][0] = 99                 # ✗ original[0][0] 也变99(内层共享)
deep = copy.deepcopy(original)     # ★ 深复制: 完全独立
deep[0][0] = 99                    # ✓ 不影响 original
# ====== 创建/复制可变结构的要点 ======
# 1. 创建"n个独立的可变对象": 用列表推导 [新建表达式 for _ in range(n)], 别用 [x]*n;
#    - [[0]*c for _ in range(r)] ✓ (r个独立的行)
#    - [[0]*c] * r              ✗ (r个引用指向同一行)
#    - [{} for _ in range(n)]   ✓ (n个独立的dict); [{}]*n ✗;
# 2. [x]*n 只适合 x 是【不可变】的(int/str/tuple) 或 你确实想要共享同一对象;
# 3. 复制嵌套结构: 浅复制(切片/copy())只复制外层, 内层可变对象共享; 要完全独立用 copy.deepcopy;
# 4. 心里清楚"我现在是想要'共享同一个'还是'各自独立的'": 想独立就别用复制引用的方式;
# 5. 同类坑: 可变默认参数(def f(x=[]))、dict.fromkeys(ks, [])(所有键共享同一list) → 都用推导/None判断。

# 核心: 创建多个独立的可变对象用列表推导(每次迭代新建), 别用 [x]*n(复制引用、共享同一对象);
#   复制嵌套结构按需选浅/深复制(deepcopy完全独立); 时刻清楚自己要的是"共享"还是"独立"。

修复的核心,是"创建多个独立可变对象用列表推导,别用乘法"正解一:用列表推导创建二维列表——[[0]*3 for _ in range(3)],推导式每次迭代重新求值、生成全新列表,三行独立,改 grid[0][0] 只改第 0 行正解二:复制嵌套结构按需选浅/深——浅复制(切片/copy())内层共享,要完全独立用 copy.deepcopy要点:创建 n 个独立可变对象用列表推导别用 [x]*n、[x]*n 只适合 x 不可变或确实想共享、复制嵌套要完全独立用 deepcopy、心里清楚要"共享"还是"独立"、同类坑(可变默认参数/dict.fromkeys)归根结底:创建多个独立的可变对象用列表推导(每次迭代新建),别用 [x]*n(复制引用、共享同一对象);复制嵌套结构按需选浅/深复制(deepcopy 完全独立);时刻清楚自己要的是"共享"还是"独立"。

第三件事:Python 引用与可变性相关的其他常见坑

排查后我把 Python 中引用、可变性相关的其他坑也系统梳理了一遍。

Python 引用与可变性的其他常见坑

# 1. [x]*n创建可变对象(本文): n个引用指向同一对象, 一改全改。→ 列表推导。

# 2. 可变默认参数(同350篇): def f(x=[]) 默认值在多次调用间共享。→ 用None, 函数内新建。

# 3. 浅复制误当深复制: list(a)/a[:]/copy() 内层可变对象仍共享。→ 要独立用deepcopy。

# 4. 函数内修改传入的可变参数: 影响调用方的对象(引用传递)。→ 需要不变就先拷贝。

# 5. dict.fromkeys(keys, [])/默认值为可变对象: 所有键共享同一个list。→ 推导/defaultdict注意。

# 6. 在遍历时修改列表/字典: 行为异常或RuntimeError。→ 遍历副本或用推导式生成新的。

# 7. is 与 ==: is比身份(同一对象), ==比值; 值比较用==(is只用于is None)。→ 别用is比较值。

# 8. 把可变对象当字典key/放进set: 可变对象不可哈希(list不行)/或哈希后改了出问题。→ 用不可变(tuple)。

# 共同根源: Python里"变量是引用、对象分可变与不可变"; 很多坑都源于"没分清自己操作的是'引用'还是'对象本身',
#   以及'多个变量/位置是否共享同一个可变对象'"; 当多处共享一个可变对象、又有人改它时, 就会互相影响。

# 核心: 牢记Python变量是引用、对象有可变/不可变之分; 操作时分清"共享同一对象"还是"各自独立";
#   创建多个独立可变对象用推导、复制要独立用deepcopy、值比较用==; 当心"无意中的共享"导致一改全改。

排查让我把引用与可变性的其他坑也梳理清了。一、[x]*n 创建可变对象(本文)。二、可变默认参数三、浅复制误当深复制四、函数内修改传入的可变参数五、dict.fromkeys 值共享六、遍历时修改七、is 与 ==八、可变对象当 key/放进 set它们的共同根源是:Python 里"变量是引用、对象分可变与不可变";很多坑都源于"没分清自己操作的是引用还是对象本身,以及多个变量/位置是否共享同一个可变对象";当多处共享一个可变对象、又有人改它时,就会互相影响核心是:牢记 Python 变量是引用、对象有可变/不可变之分;操作时分清"共享同一对象"还是"各自独立";创建多个独立可变对象用推导、复制要独立用 deepcopy、值比较用 ==;当心"无意中的共享"导致一改全改下面这张图,是这次列表乘法坑的成因与解法:

第四件事:复制层次对比表

这次踩坑后,我把 Python 几种"复制"的层次对比成一张表。

方式 复制了什么 内层可变对象 改内层会互相影响吗
b = a(引用复制) 啥也没复制(同一对象) 共享 会(完全是同一个)
[x] * n / 切片 / copy()(浅) 最外层结构 共享引用 会(本文的坑)
copy.deepcopy(a)(深) 递归所有层 各自独立 不会
列表推导新建 每次新建独立对象 独立 不会

这张表把复制的层次钉清了。核心是:"复制"这个词,在 Python(乃至很多语言)里是分"深浅"的、是个连续谱——从"啥也没复制(只是又一个引用)",到"只复制了表面一层(浅)",到"彻底复制了每一层(深)";而 bug 往往出在"我以为我复制出了一份独立的, 实际只复制了表面、内里还和原来共享着"它给我的最大启发是:"复制一份"这件事,远不是"有/无"的二元,而是有"程度/深度"的——"独立到什么层次"才是关键;当你说"我要一份副本"时,要想清楚"我要的是'表面独立、内里共享'(浅), 还是'彻底独立'(深)?";"以为复制了就独立了", 而忽略了"复制的深度", 是共享类 bug 的常见来源这给了我一种处理"副本/隔离"的清醒:每当我需要"一份独立的副本"(为了改它不影响原件、为了隔离)时,要明确地问"我需要的隔离, 要到哪一层?表面够吗, 还是要彻底?",并选用对应深度的复制——需要彻底独立(尤其嵌套结构)就用深复制, 别用浅复制糊弄;"明确所需的隔离深度、选用相应深度的复制",是避免'以为独立实则共享'的关键认清复制分深浅是连续谱、明确所需隔离深度选对应复制——是这个坑带给我的认知。

第五件事:这次事故暴露的"看起来一样,本质却共享"

这次让我反思更深一层:那三行打印出来一模一样,本质却是同一个。我把"看起来多份"和"实际几份"对比成表。

维度 [[0]*3]*3(本文) [[0]*3 for _ in range(3)]
打印出来 [[0,0,0],[0,0,0],[0,0,0]] [[0,0,0],[0,0,0],[0,0,0]]
看起来 三行一样 三行一样
实际是几个对象 1 个(被引用 3 次) 3 个独立的
改一个内层 三行全变 只变一个
区别在哪 看不出来(要看 id) 看不出来(要看 id)

这张表道出了最迷惑的地方。核心是:这两个二维列表,"看起来"(打印出来的样子)一模一样,可它们的"本质"(内部是几个独立对象、是否共享)截然不同;而这个决定性的差异,从表面(值)上完全看不出来,只有去看"身份(id)"、或者"改一个看会不会影响别的"才暴露它给我的深刻启发是:两个东西"当前的值/表现一样",完全不代表它们"背后的结构/共享关系一样"——"值相同"是表面的、静态的;"是否共享同一底层、改一个会不会动另一个"是深层的、动态的;很多隐蔽的 bug, 正藏在"表面值相同、但底层共享关系不同"的地方——它们平静时一模一样, 一旦有人"修改", 隐藏的共享关系才以"一改全改"的形式暴露这给了我一种审视数据结构的清醒:判断两个/多个数据"是不是真的独立",不能只看"它们现在的值是否一样",而要看"它们背后是不是同一个对象、改一个会不会牵动另一个"——关注"共享关系/别名(aliasing)",而非只看当前的值;"看穿表面值相同之下的共享关系差异、警惕意外的别名",是理解和调试涉及可变状态、引用的程序的关键能力认清值相同不等于结构独立、要关注共享关系而非只看值——是这个列表乘法坑带给我的认知。

第六件事:要创建/复制可变结构时,我现在的自检习惯

现在每当我要创建或复制一个含可变对象的结构,我都会先按这张图问自己:

这张图的精髓,是"创建多个可变对象用推导别用乘法,复制要彻底独立用 deepcopy"创建多个可变对象用列表推导、x 不可变[x]*n 可以、复制要彻底独立deepcopy、浅复制记住内层共享这套习惯,让我从"初始化二维列表随手 [[0]*n]*m"变成了"创建多个可变对象一律用推导式"——核心始终是:创建多个独立的可变对象用列表推导(每次迭代新建),别用 [x]*n(复制引用、共享同一对象);复制要彻底独立用 deepcopy。

我立下的几条规矩

这场"改一个格子整列都变"的事故,换来了我写 Python 时,刻进骨子里的几条铁律:

  1. Python 变量是引用,对象分可变与不可变。这是理解一切共享问题的基础。
  2. [x]*n 复制的是引用——n 个引用指向同一个 x,不是 n 个独立副本。
  3. x 是可变对象(list/dict)时,[x]*n 会让"多份"共享同一个,改一个全改。
  4. 创建多个独立的可变对象,用列表推导 [新建表达式 for _ in range(n)]。
  5. [x]*n 只适合 x 是不可变的(int/str/tuple),或你确实想共享同一对象。
  6. 复制嵌套结构:浅复制内层共享,要完全独立用 copy.deepcopy。
  7. 判断数据是否独立,看共享关系(id/会不会一改全改),而非只看当前值。

写在最后

回头看,这场由"一个图省事的列表乘法"引发的、一改全改的事故,真正教给我的,远不止"用列表推导建二维列表"这一个技巧。它让我对"'复制'这个我们以为天经地义、轻而易举的动作, 背后藏着'到底复制了什么、复制得有多深'的微妙; 而'看起来复制出了多份独立的', 可能只是'同一份被引用了多次'",有了一次刻骨的体会。我栽跟头,是因为我对"复制"有一个过于简单、想当然的理解——"* 3 嘛, 就是复制三份呗";我脑子里的"复制",是那种"像复印机一样, 印出三张一模一样但各自独立的纸"的朴素画面;可计算机里的"复制", 默认往往是''的、复制的是'引用'——它给了我"三个指向同一张纸的便签", 而不是"三张独立的纸";我以为我有了三份能各自涂改的拷贝, 实际我只是给同一份东西, 贴了三个标签这让我领悟到一个关于"复制的深浅与共享"的深刻认知:在与"引用、对象、内存"打交道的世界里,"复制"不是一个简单的、必然产生'独立副本'的动作——它默认常常是""的, 复制出的"多份", 在更深的层次上可能仍然共享着同一个底层对象;"我复制了一份" 不等于 "我得到了一份和原来彻底无关、互不影响的独立体";"共享" 常常悄悄地、默认地存在着, 而你以为你拥有的是"独立"这给了我一种处理可变状态的根本审慎:每当我"复制"一份可变的数据、或"创建多个"可变对象时,要清醒地问"它们是真的各自独立, 还是在某个层次上共享着同一个底层?我对它们的修改, 会不会意外地互相影响?"——需要独立就用确保独立的方式(推导/深复制), 而不是想当然地以为"复制了就独立了";"清醒地区分'独立副本'与'共享引用'、明确并掌控数据间的共享关系",是写对一切涉及可变状态的程序的根本基础认清复制默认常是浅的、复制的多份可能仍共享底层、要清醒区分独立与共享——这,是我用一次列表乘法的事故,换来的、关于 Python、也关于如何掌控数据共享关系的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次初始化二维列表时,把 [[0]*c]*r 换成 [[0]*c for _ in range(r)],那我对着那"改一个变一列"的计分板排查的这段时间,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我给 Agent 写了个清理 N 天前数据的工具,模型某次把 N 填成了 0,工具没校验就照单执行,把全部数据都删了:一次 Agent 工具参数未校验、把模型输出当可信输入的深度复盘

2026-6-2 22:01:42

技术教程

我用双等号判断年龄是不是 0,结果表单没填(空字符串)的也被判成了 0,因为 JavaScript 的 == 在背后偷偷做了类型转换:一次 JS 宽松相等隐式转换的深度复盘

2026-6-2 22:12:28

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索