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

前几天改一段脚本,给函数挂了个空列表当默认参数,盘算着每次调用都从干净的零开始累加。本地试了两回,安安静静。谁料一上服务器就闹脾气——从第二次起,新数据总黏着上一轮的残影,像潮水退了又涨,沙上却留着前一次的脚印,怎么也抹不平。

翻了半天才恍然。Python 的默认参数在定义那一刻便求好值,那对方括号自始至终是同一个对象,所有调用分着同一份状态,并非每次新建一份。这坑偏就藏得浅又深:本地手测未必撞见,线上多次触发才露马脚,叫人摸不着头脑。

后来学乖了,用 None 当哨兵,进函数体再 list() 起头,便天下太平。类里也是一理,别把可变对象挂成类变量默认值,否则满园实例共用一个,够你熬到天亮。
坦白讲
小事一桩,记一笔,免得哪天又踩回原处。

random_cat
[链接]

我当年也踩过这坑,还是被同事 review 代码时当场抓包,社死现场

stack14
[链接]

这坑真够阴的,本地手测十次都不一定撞见,一上服务器就露馅,我之前也被它咬过。

补一句:不光 list,dict、set 当默认值全是同一条路数,根子都在 def 那一行把同一个对象绑死了。想从根上防住,pylint 的 W0102 或者 flake8 配个 bugbear(B006)开着,写代码时直接标红,比等线上踩雷省心多了。

lyric_77
[链接]

你那句’沙上留着前一次的脚印’一下把我钉住了。总以为每次重来都是崭新的白纸,其实好多东西在看不见的地方早连成了同一根线,所谓默认的’空’,未必真空。

想起有回夜里循环一首老歌,以为换种心情就能当没听过,旋律一响,前头的情绪全涌回来,和新的缠在一起,分也分不开。大概干净的开始,从来都是自己一厢情愿的错觉。
说实话
你后来用 None 当哨兵,倒像跟生活认了半截输又不甘心,明知共用一份,偏要每次手动清空,固执得让人想笑。这帖我存着了。

duckling_de
[链接]

本地试两回就敢上线的勇气我服了哈哈 我前阵也在这坑里泡一晚上 现在默认参数全用None了

potato__40
[链接]

本地跑得好好的上线就露馅,这落差我太熟了哈哈 上次也是被这坑教做人

veteran_owl
[链接]

你那句"沙上留着前一次的脚印",我读着直点头。我头回撞见这毛病的时候,还当是自己眼花了——明明没喂参数进去,它肚里却揣着上回的剩货。蹲在那想了小半天,才咂摸出味来。
怎么说呢
这事后头见得多了,反觉得它不算坑。Python把"定义时求值"这事摊得太明白,明摆着告诉你:我只在进门那一下子算一回。是咱们总拿人的脾气去套它,以为每次敲门都该是个干净的屋子。
那会儿
我年轻时候写东西也急,巴不得一行顶三行,偏这种"省事"的默认参数最招灾。如今慢下来了,宁可在函数里现起一个空家伙,也懒得跟那份共享状态较劲。

类变量那桩你也提得在理,满园共用一个,调起来真够熬的。

noodle_bee
[链接]

前排。本地安分线上闹脾气,这坑专挑你以为没事时露脸哈哈

aurora_629
[链接]

你写"潮水退了又涨,沙上却留着前一次的脚印"那句,我盯着看了好一会儿。
怎么说呢
我也算在这类坑里扑腾过。有回写了个小玩意儿,本以为每次运行都是白纸,结果旧的数据像没散干净的烟,一圈圈绕着新来的转。后来才晓得,有些东西在它落地的那一瞬就被定了形,往后所有相遇,其实都是同一场重逢。

Python 这脾性,倒叫我生出一点念想:人大约也是这样。说实话某些念头在心底落定的那一刻便成了定值,此后每次想起,踩的仍是上一条旧辙,带着前一次的回响。

楼主用 None 当哨兵的法子利落,我记下了。只是私下觉得,那点残影未必全是麻烦,脚印抹不平,不也正好说明潮水曾来过。

classic
[链接]

这坑我早些年也踩过,不过是从另一头摔进去的。

话说回来你讲的那层道理都对,默认参数在定义时求值,所有调用共享同一个对象。我想补一句:这行为其实不是设计者没想清楚留下的 bug,反而是他们故意这么定的。早些年我见过有人拿它当缓存使——默认参数里挂个字典,函数跑过的数据往里塞,下次进来先查,省得重复算。那阵子还觉得挺巧,后来接手的人没一个敢动那函数,谁也说不清里面攒了些什么。同一个机制,用歪了是坑,用对了是个 feature,关键看写的人心里清不清楚。

你说本地试两回安安静静,这点我特别有同感。我觉得吧这类毛病的共同点是:在单次、少量的调用下绝不露脸,非得状态攒够了、调用次数上去了才现原形。不光默认参数,类变量那桩也是一理,再往外推,闭包里套循环变量、模块级的全局可变状态,全是一个病根——可变的东西被当成了共享资源,却没人盯着它的生命周期。

我后来养成的习惯是,凡看到函数签名里挂着可变默认值,第一反应不是"它会不会出问题",而是"我有没有必要让它有状态"。八成时候是不需要的。真要存点跨调用的东西,宁可显式传进来,或者挂到实例上、用个专门的缓存对象,清清楚楚摆在台面上。None 当哨兵固然好,但根子上还是那句话:别让隐式的共享状态替你做决定。

你记这一笔挺值。我那头更闷,有段脚本靠这个"特性"跑了一年多没出事,直到有天并发上来了,两个调用互相踩,排查那通宵才想起根在这。这种坑就这样,平时不响,一响就是半夜。

phd__sr
[链接]

顺着你那句『本地手测未必撞见,线上多次触发才露马脚』,延伸一点:这个归因方式其实值得商榷。根子不在『本地』和『线上』两种环境,而在于函数对象是否常驻内存。

默认参数在模块加载、函数定义那一刻求值,对象引用就钉死在 func.defaults 里了,跟运行环境无关。本地若在同一解释器会话里连调两次以上,照样会黏上上一轮的残影;反过来,线上若是每来一个请求就拉起新进程、跑完即焚,那默认列表每次都是全新的,反而不出问题。

所以更准确的说法是:只要『一次定义、多次调用』发生在同一进程生命周期内,共享状态就会暴露,跟是不是服务器没有必然关系。你踩到的本质是这个,Python 官方 FAQ 里『为何默认参数会被共享』一条也是这个解释。

None 哨兵那条,func.defaults 存的就是 (None,),进函数体再 list() 或 [] 都行,二者等价,挑顺手的来就好。

quant_cat
[链接]

有个地方值得商榷。你说的"本地安安静静、上服务器才闹脾气",这个对比容易让人误以为是环境差异,其实行为是确定性的,跟在哪台机器上跑没关系。真正左右它显不显现的,是同一进程里这个函数被调用了几次。你本地试两回没撞见,多半是脚本跑完进程就退了,那块默认参数随进程一起没了;服务器上八成是常驻进程反复走同一段逻辑,第二次进来默认参数那个对象还热着,自然就黏上上一轮的残影。

想验证的话很简单,本地写个 def f(x, buf=[]): buf.append(x); return buf,紧接着连调两次 f(1) 再 f(2),回来看返回值,马脚立刻露,根本不用上服务器。

类变量那句我顺带补一刀:只要在 class 底下直接挂的可变对象,实例间共享是铁律,不止默认值这一个坑。先记着,后头有空再聊。

couch_197
[链接]

这坑我也栽过,去年冬天的事。写了个小脚本处理点东西,图省事拿空dict当默认参数,跑了一夜发现结果全串了,对着屏幕愣半天。Genau! 就是这股味儿——本地怎么测都乖,一上量就露馅。
唔
诶后来也是用None兜底才安生。说真的这种bug最磨人不是难,是它看着明明对,越看越觉得自己没问题,越看越怀疑人生,咖啡续到第三杯才调通。

楼主记一笔挺好,我也顺手贴上冰箱了,免得哪天又掉回同一个坑。话说你们有没有反复踩同一个坑的,我怀疑我脑子对方括号有某种执念哈哈

sage
[链接]

想当年我自学时也在这坑里泡过半天。可变默认参数嘛,吃过一回,往后写函数自然就留神了。

sonnet69
[链接]

你那句’沙上留着前一次的脚印’真叫人心里一动。同一份状态被反复翻搅,像旧信纸叠了新墨,总透出底下的字影。改完那一刻,总算松口气了吧。

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