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

前阵子改一段老代码,有个函数参数是个空列表当默认值。我一开始没当回事,第一次调用往里塞了几个元素,一切正常。结果第二次再调,列表里居然还留着上回的内容,数据像凭空多出来似的,查了半天才反应过来。

没事的这其实是 Python 一个挺经典的小陷阱。默认参数不是每次调用都现造一份,而是在函数定义那一刻就求值一次,之后这个对象一直挂在函数上。列表是可变的,多次调用之间就共享了同一个,你往上加东西,下次进来它还好好在那儿等着。表象就是头一回没事,从第二回起数据开始不干净,极难察觉。

后来我学乖了,需要可变默认值的场合,一律先写 None,进函数体里再判断初始化。或者干脆用元组这种不可变的,从源头上不给自己留坑。

这个小坑藏得真深,你们有没有被类似的东西绕进去过,说来听听让我心里平衡一下 ( ̄▽ ̄)

quant_bee
[链接]

从某种角度看,"定义那一刻求值一次"这个说法值得商榷。准确讲是默认参数表达式在 def 语句执行时才求值,并不是字面意义的定义时刻。多数情况下 def 写在模块顶层只执行一遍,所以效果一样;但若这个函数包在另一个函数体里、外层被反复调用,每次都会新生成一个函数对象和一份默认列表,那个坑反而不一定复现了。我前几年就栽在这个边界条件上,排查了挺久。

stone
[链接]

我年轻那会儿也在这上头栽过跟头。那时候写个小脚本处理数据,函数默认参数塞了个空字典当缓存,头几次跑都正常,后来越跑越慢,查到最后才晓得是同一个对象一直在背后累加。你最后那个法子我后来也这么干,默认值先写 None,进函数里再开新对象,省心。

这种坑踩过一回就记一辈子了,当时真把我绕了好几天。你们谁还被别的语言坑过,说出来让我也平衡平衡 ( ̄▽ ̄)

lazy
[链接]

凭空多出来那段我当年也破防过,对着diff看了半天愣是没看出所以然

stack29
[链接]

不止 list,dict、set 一个样。这坑我早年也踩过,不过偶尔故意拿它当缓存还挺好用。

elder77
[链接]

这个坑我年轻时候也踩过,不过当时是在另一门语言里,机制不完全一样,可那种"数据凭空多出来"的毛骨悚然感,记了很多年。

你摸到根上的那句话是对的,默认参数在定义那一刻就求值一次。顺着这根线再往下捅一层,函数对象本身是个活物,它的 defaults 一直挂在那儿,多次调用其实是在同一个对象上进进出出。所以表象才那么阴,头一回没事,第二回起数据就不干净,藏在眼皮底下。嗯…

想补两点,算同一坑的旁支,不是唱反调。

一是你那个"先写 None 再进函数体初始化"的写法,自己还藏着一个暗雷。有人图省事写成 arg = arg or [],这下麻烦了,万一你真想传个空列表、或者传 0、传空字符串当合法输入,全被 or 判成 falsy 给吞掉,又是一个静默的错。还是老老实实 if arg is None 最稳当。

二是这陷阱换个马甲还会出现。类属性写了个空列表当默认,所有实例共享同一份,你在 a 上 append,b 也跟着变。说实话还有 loop 里 lambda 抓循环变量那种 late binding,现象很近,说白了都是"引用共享"这一族的。别急

根子不在 Python 设计得刁钻,是我们脑子里默认以为"默认等于每次现造一份"。把 reference 和 value 分清楚,大半坑就自己没了。
怎么说呢
话说回来,偶尔我还真见过有人反过来拿函数默认参数当 cache 用,能跑,但状态藏得深、又不线程安全,看得我后背发凉。你们谁干过这种野路子,我倒想听听。

dr_632
[链接]

我当年头回踩这坑,也是查了半天才回过味来。补一句,你说的"因为列表可变所以共享了同一个",因果其实反过来了,共享并非 mutable 导致的,而是默认对象在函数定义时就造好、此后一直挂在 func.defaults 里,每次调用拿到的都是同一份引用。可变只是让这层共享暴露出来,你从不改它根本看不出来。

所以"换元组"得补半句:元组不可改,真要往里加东西还是得在函数里另建 list,绕一圈还是回到 None 那条路。我后来固定用 if x is None,不写 if not x,后者会把 0、空串都当没传,那又是一坑。

stone72
[链接]

我年轻时候也在这类地方绊过脚。那时候改一个老脚本,默认参数挂着的列表跨调用越攒越多,我还当是数据自己长了腿。后来规矩就一条:要变的东西别往默认参数里塞,进门再安顿。你那个先写 None 的法子,稳当。

lyric__cn
[链接]

那个空列表像一只不肯倒空的杯子,第一次倾进去的茶,第二次还在杯底沉着。我也被这类幽灵缠过,盯着屏幕到天亮才明白

sage_259
[链接]

这坑我早年也踩过。仔细想想头回没事,二回数据就脏了,查半天才反应过来,跟你一模一样。后来我也学乖,进函数再初始化,省心。

brutal28
[链接]

这坑我当年也中过招,而且比你还隐蔽——默认参数是个 dict,调了几十次才发现所有调用都往同一个对象里塞东西,排查时差点把头发薅光。说真的 Python 这 “定义时求值一次” 的设定对头回撞上的人太反直觉了,你反应已经算快的,我当年愣是绕了一下午才回过味来。

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