一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
默认参数,背着我长了个数组
发信人 newton_64 · 信区 灵枢宗(计算机) · 时间 2026-09-28 15:53
返回版面 回复 14
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
newton_64
[链接]

上周帮朋友调一个Python小脚本,现象很怪:函数明明只在本次调用里往列表append了一条数据,打印出来却带着上一次的旧值。我第一反应是数据没清空,查了一圈,变量都是局部作用域,没道理串。

后来逐行捋,才发现问题出在函数签名上:def f(items=[])。Python的默认参数是在函数定义时创建一次,之后每次调用复用的都是同一个对象。你往里头append,等于在所有人共享的那个列表上做手脚。第一次调用写进去的东西,第二次调用原封不动地还在那儿躺着。文档里其实白纸黑字写着这条,只是大多数人包括我,初学时都扫一眼就翻过去了。

正确做法也简单:默认值写成None,进函数体里判断一下,是None就新建一个空列表。或者干脆用元组这类不可变对象当默认值,从根上断了这个念想。

这事给我的教训是,语言的坑往往不在语法多复杂,而在它执行时机和你直觉不一致。定义时和调用时,一字之差,行为天差地别。各位还踩过哪些"文档里写了但我没看见"的坑,欢迎交流。

yolo_jp
[链接]

哎哟这坑 我当年也中过招 还以为自个儿作用域没整明白 闹半天是定义时那一下埋的雷……

couch39
[链接]

items=[] 那对括号就是个雷 我头回踩还以为是自己作用域没学明白

noodle_v
[链接]

这坑我初学也中过招,盯着打印结果怀疑人生后来养成习惯默认全写None,矫枉过正了属于是

brutal_cat
[链接]

这坑我见过不止一次。最离谱的是有人拿它当缓存使,还觉得自己挺聪明,C’est la vie。默认值老老实实写None,多两行代码,总好过半夜被共享列表吓醒。

iris_z
[链接]

读到末尾那句"定义时和调用时,一字之差,行为天差地别",手边的茶凉了半截都没察觉。这话说得真静,静得让人心里空落落的。

我觉得吧我倒想顺着你说的"文档里写了但我没看见"多唠两句。这种事后才恍然、原来答案一直摊在眼皮底下的滋味,我太熟了。年轻时读书总贪快,一行一行往下咽,自以为什么都明白了,哪晓得有些字句天生是留给后来的自己看的。前阵子重翻旧诗词,同一句当年只当写景漂亮,隔了些年事再读,忽然就咂摸出从前没看见的意味来——它从来没变过,变的是读它的人。

你那个列表也是这般。它不是在骗你,只是忠实地、一遍遍复用着定义那一刻生成的那个对象,比谁都守诺。是我们把"直觉"错当成了"约定"。

所以我想把你的教训再推一步:好些坑不是语言设的陷阱,是我们太信任自己脑子里那套默认参数了。日子里的好多事又何尝不是——有些念头、有些期待,我们在某个清晨随手设了默认值,从此每次相遇都往里添一笔,直到某天摊开来看,才惊觉怎么攒了那么多自己都没留意的旧东西。

下次谁也踩了这坑,把文档那段指给他瞧就好,比讲道理省事多了。

sweet_472
[链接]

调这玩意儿肯定折腾够呛,辛苦了。坑藏得是真深,代码看着清白其实留了后手,头回碰上都得懵一阵。

sweet2005
[链接]

嗯嗯,这种"以为清空了其实还躺着"的错愕感太真实了。我虽然不搞代码,但楼主最后那句"定义时和调用时一字之差"让我愣了下,人和人之间好多误会好像也卡在这,你以为说出口的瞬间对方就收到了,可人家接住的往往是另一个时点的你~

truth_hk
[链接]

这坑太经典了,我第一次踩也一脸懵。说真的,Python还有个孪生兄弟坑:闭包里循环变量绑定,lambda全抓最后一个值。文档也写了,可谁初学看得进去啊。你朋友运气好,有你逐行捋 ( ̄▽ ̄)

maple_2000
[链接]

我刚学那会儿也掉过这坑,盯着自己长出来的列表愣了好久。定义时和调用时这茬,真容易想当然。

haha_ism
[链接]

这坑我当年自己瞎鼓捣python的时候也栽过,卡了半天才反应过来是同一个对象在作妖,打印出来一串旧数据差点把我整怀疑人生

不过琢磨久了我觉得吧,这玩意儿压根不算bug,是人家设计时就这么定的。python里函数也是对象,默认参数就挂在函数对象的__defaults__上头,定义那一下算一遍就钉死了。你真想要"每次调用都重新生成"的默认值,那才是特例,得手动写None再去建。所以python的逻辑其实自洽,只是跟咱脑子里"每次调用都初始化一遍"的直觉打起来了。6

顺着"定义时 vs 调用时"这条线,我想起另一个亲爹级大坑:循环里写lambda。就 fs = [lambda x: x*i for i in range(3)] 这种,你以为fs[0]乘0、fs[1]乘1,结果全返回6,因为i是延迟绑定的,等真调用的时候循环早跑完了,i停在最后一个数上。这跟默认参数的毛病基本一个妈生的,都是"你以为的时机"和"实际的时机"对不上。

所以我后来养成个习惯,碰上默认值、碰上闭包引用外层变量,先问自己一句:这玩意儿到底啥时候算的?想明白这句,python一大半阴间行为都能顺过来。呢

你们有没有碰过那种不怪逻辑、纯纯typo型的坑,我之前把range写成rang愣是debug一小时,太丢人了哈哈

sage40
[链接]

我年轻的时候也信过"默认参数就是个初始值"这个念头。那会儿写个小工具,def f(buf=[]) 拿来当缓冲区用,还觉得自己省了传参挺巧妙。结果越跑越不对,同一个函数前后两次调用串了味儿,盯了一下午才想明白:这列表是定义那一下建好、之后每次调用共用同一个对象。跟你帖子里说的,一模一样。

楼主讲"定义时和调用时一字之差,行为天差地别"…,我挺有感触。这事儿你说它是坑吧,其实设计上挑不出毛病。求值时机是语言设计者的取舍,没有绝对对错,有些语言就偏在每次调用时新建。问题是它把"定义时求值"藏得太不显眼,名字又起得温和,叫默认参数,听着就像一个无所谓的初始值,谁会防着它。
坦白讲
我后来落了个习惯:看见"默认"两个字,不管哪门语言,先停一下。默认值不是白给的,背后多半有个对象在你看不见的地方活着。撞过一回,自然就长记性了。

你那朋友这脚本,后来改成 None 兜底了没?

noodle_cat
[链接]

这坑我熟哈哈 第一次中招的时候盯着屏幕发了十分钟呆。不过比起"记得用None"那句结论,我更想接你最后说的——定义时和调用时的差。这玩意儿真不是Python单独丢给你的一个坑,是一窝。

你就把 def 那行当成分界线,一下子就通了:def 这一行上写的东西(默认参数算在内),是解释器读到这行就一次性算好的;函数体里那些才是每次调用现跑。那个[]属于"def这行"阵营,所以只生一次,之后所有调用共用同一个对象,你往里append等于往公共储物柜塞东西。

顺着这条线往下捋,Python好多反直觉其实同一个毛病。比如循环里写 lambda:[lambda: i for i in range(3)] 三个函数全吐 2,因为 i 是晚绑定,真调用时才去抓当前值。再比如 x is None 跟 x == None 平时长得一样,但 None 是单例,得用 is——这又扯到小整数池、字符串驻留那堆"到底是不是同一个对象"的破事。

说回可变默认参数,我想补一刀:它是双刃。真想跨调用存点状态,早年间没装饰器,就有人故意拿 def f(cache={}): 当缓存用,那个字典在调用之间一直活着,效果跟后来的 @lru_cache 差不多。所以教训未必是"别用可变默认值",而是"先想清楚你要共享状态还是每次全新的,再决定默认值怎么写"。陷阱和特性穿同一件衣服。嘿嘿

还有个点…,这坑在别的语言不一定是坑。JS 的 ES6 默认参数是每次调用现算的,function f(items=[]) 每回给你新数组。你几种语言切着写,光背"默认参数会串"反而乱,得记住这是 Python 自己选的实现方式。

对了你们还有没有那种看文档觉得理所当然、真踩了后背发凉的?我最近被 slots 跟继承搅一起的事整得够呛,改天单开一帖吐哈哈~

dear2001
[链接]

嗯嗯, 这个坑我当初也掉进去过, 还耽误过事儿。有回自己写了个小脚本处理表格, 函数里拿默认列表攒数据, 批处理的时候前面文件的结果全混到后面去了。当时怎么都想不到是默认值在作怪, 一度怀疑是不是文件读串了。后来也是一行行捋才反应过来, 那种"原来是它"的无奈, 跟楼主一模一样。

说起来Python里还有个挺像的坑, 就是lambda里引用循环变量。for i in range(3) 包三个lambda, 真调用的时候i早就跑完了, 三个函数拿到的全是最后那个值。嗯嗯根子也是楼主说的"定义时和调用时", 对象在定义那一刻就绑上了, 等你真正用的时候, 已经不是你以为的那一刻了。

会好的不过话说回来, 这种坑踩过一次就记一辈子, 比翻十遍文档都牢。我后来养成个习惯, 看见函数签名里默认值是可变对象, 先心里咯噔一下 ( ̄▽ ̄)~

drive
[链接]

顺着你"复用同一个对象"那句…,我较个小真:更精确的说法是那个列表被绑定到了函数对象的 defaults 属性上,跨调用复用的不是什么玄学的共享状态,而是这个函数默认的同一份参数槽位。单线程里它表现为"残留旧值",放到多线程场景里,它同样是被多个调用方共同持有的可变状态。
其实
另外关于"文档里白纸黑字",我倒想替官方教程说句公道话——那段其实是被加粗标成 Important warning 的,不算藏在角落里没人看见。所以教训也许不是文档没写,而是我们初学时对加粗警告的阈值设得太高了(笑)。

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