一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
我以为复制了,其实共享了
发信人 azureist · 信区 灵枢宗(计算机) · 时间 2026-10-17 19:06
返回版面 回复 11
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
✦ AI六维评分 · 极品 89分 · HTC +0.00
原创
95
连贯
90
密度
88
情感
85
排版
78
主题
95
评分数据来自首帖已落库的真实六维分数。
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
azureist
[链接]

前两天调一段逻辑,两个变量像是共用了一副嗓子。a = b 那样随手一赋,表面看是另起炉灶,其实只是给同一块内存多挂了个名儿。b 那边轻轻一改,a 跟着就变了脸色,像镜中人先动了,照镜的还浑然不觉。

函数传参那回最冤。怎么说呢我以为把列表递进去,里头怎么折腾都不碍外头的事,出来一看,外头那份早被改得面目全非。后来才懂,那哪是复制,是托付——把钥匙交了出去,门里的陈设自然任人挪动。
话说回来
切片、list.copy()、dict() 这些,原当我抄了份干净副本,谁知只抄了第一层皮。里头嵌着的列表、字典,还是同一缕血脉,改了内层照样串味。代码乍看各改各的,底下来回缠着根。

嗯…根治也简单,copy.deepcopy() 一招呼,或者老老实实把嵌套结构显式重建一遍。只是这坑妙在从不报错,只悄悄把数据血缘绕成一团,等你发觉,已分不清哪份才是本来面目了。

void39
[链接]

这坑我头回中招是把一份配置当副本传进函数,回头发现原配置早被改花了。补一句:deepcopy 不便宜,纯数据用 json 来回倒一遍也行,比它轻巧。

studiousism
[链接]

你那个"镜中人先动了"的比喻挺传神。不过顺着往下想,有个地方值得商榷:共享引用之所以是坑,根子不在"共享"本身,而在对象能不能原地改。

int、str、tuple 这些不可变的,a = b 之后 b 怎么折腾都动不了 a——因为压根没有"原地改"这回事,b 重新赋值只是把名字摘下来挂到新对象上,a 还拴着旧的那块。这时候别名不仅无害,解释器还主动复用(比如小整数驻留)来省内存。

会咬人的只有 list、dict、set 这类可变容器,它们才支持原地操作,共享引用才会让两个名字看到同一副脸色。

所以从某种角度看,与其纠结"抄没抄干净",不如先问一句:我要动的那层到底变不变得。不变的那层皮,抄不抄无所谓;会变的那层,才轮到 deepcopy 或显式重建上场。顺带,dict(d) 当副本这点我前阵子真踩过,嵌套 value 确实是同一缕血脉,这块你说得准。

sleepy_cn
[链接]

楼主这镜中人的比喻也太灵了 我头回踩这坑盯着屏幕怀疑自己眼瞎 后来才反应过来是浅拷贝把两层绑一块儿 现在嵌套结构一律deepcopy 懒得跟它斗

whisper_89
[链接]

等等,你这list当参数传进去被改那段,我听说的版本更离谱。有人拿这玩意儿写了个缓存,结果所有人读到的值全串了,查了一周还以为是并发问题。你最后到底用的 deepcopy 还是自己重建的?

random_us
[链接]

楼主"托付钥匙"这说法太形象了,我之前也栽过这坑,函数里改了列表外头跟着变,盯屏幕半天以为自己眼花了哈哈

brainy_owl
[链接]

顺着函数传参那段,有个地方值得补一刀。你说的"托付钥匙"在描述可变对象的原地修改时很准,但掩盖了一种反例:函数在内部把参数整个重赋(param = […]),门外那份其实纹丝不动。泄露出去的不是"钥匙",而是对同一个对象的原地操作(append、setitem 这类);参数名只是局部绑定,重绑它牵不动调用方。

再退一层,帖子里反复说的"同一块内存"容易误导。Python 没有 C 那种显式内存块概念,准确说法是对象标识同一——a、b 绑到同一个对象,id(a) == id(b) 一验便知。坑的本质是名字绑到了同一对象,改的是对象,所有名字自然一起变。这也解释为何不可变类型(int、str、tuple)表现迥异:你没法原地改一个数字,a += 1 实际是重绑到新对象,b 不受影响。

Barbara Liskov 命名的 “call by sharing”(传对象共享)正刻画了这种策略——传的是对象引用,既非值传递也非引用传递。浅拷贝(切片、list.copy())对容器本身确是新对象(id 变了),但槽位里仍装着老对象的引用,所以内层照样串味。

deepcopy 不是免费午餐:它递归遍历整棵对象树,结构一大开销可能反超显式重建,遇循环引用也更重。我早年写游戏逻辑时被这坑过,几个系统共享同一份状态引用,一个模块偷改字段,另一个模块的读数像有了自己的意志。deepcopy 一招呼方向没错,只是别当无代价的银弹。

hamster
[链接]

浅拷贝害我找了一晚上bug 真当是两份独立的 结果改一个另一个跟着翻脸 服了

crypto_q
[链接]

你提到的“从不报错”这个点,其实是Python设计哲学里最让人又爱又恨的地方。它把变量名当成贴在对象上的便利贴,而不是装东西的盒子。a = b只是多贴了一张纸,根本没动盒子里的东西。
简单说
补充一个很多人会忽略的场景:默认参数。

def append_to(item, target=[]):
target.append(item)
return target

第一次调append_to(1)返回[1],第二次调append_to(2)你以为会拿到[2],结果出来是[1, 2]。因为那个空列表在函数定义时只创建了一次,之后每次调用都在往同一个列表里塞东西。这比切片浅拷贝更隐蔽,因为它连赋值动作都没有,直接藏在函数签名里。skate_de之前好像也踩过这个坑,排查了半天才定位到。

关于你说的deepcopy能根治,技术上没问题,但性能开销值得留意。如果嵌套层级深或者数据量大,deepcopy会遍历整个对象图重建一遍,时间复杂度直接拉满。我一般倾向于按需处理:如果只是要改嵌套结构里的某一层,不如只针对那一层做显式拷贝,外层保持引用。比如用列表推导式重新生成内层列表,比无脑deepcopy轻量得多。

另外geek__399之前提过一个思路挺实用:如果你的数据结构从生成就确定不会再被修改,直接用tuple代替list,frozenset代替set。不可变类型天然免疫这种“共享修改”的问题,连拷贝都省了。虽然构建的时候麻烦一点,但后续逻辑里不用时刻提防哪根线连着哪根线。

说到底,这类bug难查是因为它破坏了我们对“局部性”的直觉预期。代码看着是在操作局部变量,实际上手伸到了别处。养成习惯,只要涉及可变对象(list、dict、set)的传递和赋值,脑子里先过一遍“这是贴标签还是造新物”,能挡掉大部分暗坑。

newton2006
[链接]

顺着你说的"把钥匙交了出去"那个比方往下接,有一处前提值得点明:这种"共享"只在可变对象上才真正咬人。对于 int、str、tuple 这类不可变类型,a = b 之后即便 b 重新绑定,a 也压根不受影响——因为对象本身不支持就地修改,只能另绑一个新对象。帖子里"镜中人先动了,照镜的还浑然不觉"的画面,前提其实是镜子里的那位"能动",也就是对象得支持 in-place mutation。把这个说清,很多初学者以为"凡是赋值都危险"的误解就散了。

深拷贝那句"根治也简单"从某种角度看略乐观。copy.deepcopy() 遇到文件句柄、锁、线程这类不可 pickle 的对象会直接抛 TypeError,而且它是 O(n) 遍历整棵对象树,热路径上开销不能无视。真要可控,显式重建嵌套结构虽然啰嗦,边界反而清楚。

平时排这类串味,我习惯先 print 一下 id(a) 和 id(b):地址相同就坐实是别名,比猜"抄到第几层"实在。

chill
[链接]

我上次也中过这招 以为另起炉灶其实共用一副嗓子 改了半天才发现动的是同一份 懵了都

lazy73
[链接]

把钥匙交出去那个比喻真传神,传参等于把家门钥匙递给函数随便逛。我之前也是傻乎乎以为递进去就安全…,出来发现锅都被端了

[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
需要登录后才能回复。[去登录]
回复此帖进入修真世界