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

今天又看到新人代码里的经典翻车现场~他写了个函数想给 list 当默认参数,def add_item(x, items=[]): items.append(x); return items,本意是调用方不传就给个空列表。结果第一次 add_item(1) 返回 [1],第二次 add_item(2) 竟然返回 [1,2]。他一脸懵:我就想每次来个干净的空列表啊。
哈哈
这坑挺阴的。python 的默认参数不在每次调用时求值,而是在函数定义那一下就求一次值,这个 list 对象会被所有调用共享。你往里塞东西,下次调用还是同一个对象,状态全留着。吧调试时特别难查,因为代码表面看毫无毛病,跑起来却处处诡异。

正确姿势是用 None 当默认值,函数体内再初始化:def add_item(x, items=None): if items is None: items=[]。或者干脆别拿可变对象当默认值。这玩意儿刚学的时候真容易栽,栽过一次就记一辈子了。

prof_2006
[链接]

楼主把"默认参数在函数定义时求一次值"讲清楚了,不过我想补一个常被忽略的细节:这个默认值其实是挂在函数对象自己的属性上的。你跑完两次 add_item 之后,敲一下 add_item.defaults,会看到 ([1, 2],) —— 状态就明明白白躺在函数对象里,这才是"为什么状态留得住"的真正落点。调试时直接看这个属性,比对着源码干瞪眼快得多。

顺带说句"这其实是 feature 不是 bug"的视角。共享同一个默认对象是有意为之的语言设计,目的之一正是让函数能自带状态。比如想写个简单累加器:

def counter(step, _store=[0]):
_store[0] += step
return _store[0]

每次调用在同一个对象上累加,是合法用法。所以"坑"的精确边界应该是——你不该在一个会被反复调用、且你会原地修改它的可变默认值上做手脚,而不是"可变对象一律不能当默认值"。只读不写的话,list 当默认值毫无问题。

None 兜底那个写法,楼主给的是标准答案,但它只防住了"默认值是共享的那个"这一种情况。如果你写 add_item(2, [9]),函数里 items.append(x) 照样把 9 改成 [9, 2],并顺手改掉调用方手里的那个 list——因为传进来的引用和函数内是同一个。这点新人很容易以为"用了 None 就彻底安全",其实只是安全了一半。

现代写法其实更省心:Python 3.7+ 用 dataclass 的话,直接写

items: list = field(default_factory=list)

每次都现造一个新 list,语义和 None 兜底等价,还少两行判断。顺带一提,pylint 的 W0102 和 flake8 对应规则都会对这种写法报警,新人把 linter 打开能挡掉一大半这类翻车。

说到底这事儿 C’est la vie,栽过一次的人基本都刻进肌肉记忆了。你们当年第一次踩这个坑是在什么场景下?

haha2006
[链接]

대박 第二次返回[1,2]那个真离谱… 我头回见也懵了半天 这种坑就藏得贼深 不讲道理哈哈

couch2004
[链接]

这list活得跟个老住户似的,第一次调用的痕迹全住里头了,下次来人还接着用,串味串得明明白白哈哈。我当年看人代码也犯过懵,表面干干净净一跑就出怪事。栽过一回真记一辈子,Genau!

buzz_ous
[链接]

等等 这新人哪个组的 我上个月才撞见另一个人栽一模一样的坑 该不会是同一门课作业吧

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