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

昨天调一个统计函数差点把我整崩溃。代码大概长这样:
简单说
def add_item(x, buf=[]):
buf.append(x)
return buf

第一次 add_item(1) 返回 [1],看着没毛病。第二次 add_item(2) 居然吐出 [1, 2],我当场愣住。查了半天才反应过来:buf=[] 不是在每次调用时新建的,而是在 def 那一刻就创建好一个对象,之后所有调用共用同一个 list,append 全累积到同一块内存上去了。

这坑最恶心的地方是不报错也不 warning,就 quietly 把状态串了。要命的是它只在被多次调用时才暴露,单元测试往往只跑一次,根本测不出来。其实

正确姿势是用 None 当哨兵,在函数体里再初始化:

def add_item(x, buf=None):
buf = buf or []
buf.append(x)
return buf

记住一条:默认参数只求值一次。可变对象(list/dict/set)别直接当默认值,跨调用的静默共享迟早咬你一口 (´・_・`)

elder_ive
[链接]

我年轻的时候写C,哪有这种温柔的陷阱。内存要自己管,错了直接段错误,倒也痛快。Python这点确实阴,它不跟你吵,闷声就把状态串了。

你说的坑我前些年也栽过,不是函数默认值,是类属性。大概写成 class Foo: cache = {},本以为每个实例各用各的,结果全实例共享一个dict,线上跑了小半个月才有人发现计数越攒越离谱。那回之后算是真记住了:可变对象挂在"定义时"还是"运行时",差着一层天。

你最后那句"默认参数只求值一次",是根上的理。其实也有人反着用这特性当缓存使,但我总觉得别扭,能避开就避开,写得直白,夜里睡得安稳些。

hacker_587
[链接]

你那段 buf = buf or [] 还有个边角没盖住:有人显式传 add_item(2, []) 进来时,空 list 是 falsy,buf or [] 会给他换一个全新的空 list,调用方手里的引用根本不会被改动。要的是「只在没传时才新建」,if buf is None: buf = [] 才精确,只认 None 当哨兵,别的 falsy 值一律放行。想亲眼确认那个共享对象,直接 print(add_item.__defaults__) 就行。

echoous
[链接]

读到’第二次居然吐出 [1, 2]'那一瞬,我跟着愣了一下,像被人从背后轻轻拍了肩。最磨人的从来不是轰然出错,而是这种不声不响的串味,不报错,不预警,只是安静地把每一次都叠进同一块旧地方。
说实话
前年我鼓捣一个统计成绩的小脚本,也栽过一模一样的跟头。那天下午阳光白得晃眼,屏幕却冷冰冰地告诉我:你以为的崭新,其实从第一行定义起就一直活着,替你收着往后所有的落雪。

忽然就懂了’默认参数只求值一次’这句话的分量。有些东西生来一次,便再不重启。

你那招 None 当哨兵,我默默抄进备忘录了。只是有时候想想,生活里那些真正的默认参数,好像从没打算给谁重来的机会……

dev__hk
[链接]

补一个坑中坑:你那个 buf = buf or [] 的写法,在调用方真的传了空 list 时会悄悄翻车。

看这个:

Code
def add_item(x, buf=None):
简单说    buf = buf or []
    buf.append(x)
    return buf

acc = []
add_item(3, acc)   # 你以为往 acc 塞了 3

acc 是 [],在 buf or [] 里是 falsy,结果 buf 被换成全新 list,acc 还是空的。调用方以为传进去了,实际被你丢弃。正确姿势是老老实实 if buf is None: buf = []。or 这种捷径只在"调用方绝对不会传空/0/False 这类 falsy 值"时安全,而用了默认值恰恰容易让人以为传不传都行。

根因你说对了:默认参数在 def 那刻求值一次,存进函数对象的 __defaults__。想验证直接 func.__defaults__ 就能看到那个被共享的 list。这其实不是 bug,是 Python 故意的——def 是条语句,参数默认值是语句执行时算出的表达式结果,跟函数体里的局部变量不是一回事。

"测试测不出来"这点,低成本补法:让同一函数连跑两次的用例,或者上 hypothesis 做 property-based testing,随机多轮里跨调用串状态很容易暴露。mypy 帮不上,类型标注管不了运行时对象共享。

最阴的就是它不报错。我现在 code review 直接把 def f(x=[]) 当 red flag,CI 挂个 ruff B006(mutable default)直接拦,比人肉 review 稳。

lol_2004
[链接]

我当年创业写后台也栽过这坑 排查半天才想起来是可变对象默认值在作祟 这种不报错的静默bug最搞心态了

crypto_owl
[链接]

你那个 buf or [] 还有个隐患:调用方要是真传个空 list 进来,空 list 是 falsy,会被直接丢掉换成新的。更稳的写法是 buf = buf if buf is not None else []。我之前就踩过这个 falsy 的坑。

dr_dog
[链接]

你文末那个修复写法,从严谨角度看还有个边界情况值得单独拎出来说。buf = buf or [] 在绝大多数调用里没问题,但 or 判断的是「假值」而不是「没传参」。如果有调用方显式传了一个空列表 [] 进来,[] 在布尔上下文里是 False,于是 buf 会被重新赋成一个新 list,对方传进来的那个对象就静默丢掉了。这种 case 单元测试更不可能覆盖到,真踩了会非常迷惑。

更稳妥的是显式判 None:

def add_item(x, buf=None):
if buf is None:
buf = []
buf.append(x)
return buf

只有「没传参数」才新建,传了什么就用什么,语义最干净。대박 这种默认参数只求值一次的设计,官方教程里都专门拿它当反面例子讲,确实是 Python 最容易误导新人的点之一。

maple_2000
[链接]

这种不报错却悄悄把状态串起来的 bug 最磨人心态了,我之前也被默认 dict 坑过一回。后来只要看见可变对象当默认参数就条件反射改成 None,多写两行但 sleep 踏实多了 (´・ω・`)

radar_fox
[链接]

你们知道吗,这个坑在 Python 邮件列表当年吵得可凶了。我听说 Guido 是有意这么设计的,默认参数在 def 时绑到函数 defaults 上,本意是让函数能当闭包使、甚至运行时改默认值。这 feature 在有些人眼里挺 nice,只是九成九的时候它反手咬人。

楼主说单测抓不到我特别信。我有个哥们就栽过,线上跑一周才发现计数越叠越歪,查出来那表情跟你愣住一模一样 (´・_・`)

现在见 buf=[] 我基本直接当 red flag。话说回来,你们觉得该怪语言设计,还是怪写代码的人太 trust 这个 silent 行为?

bored_128
[链接]

这坑我也踩过,盯着屏幕半天没反应过来是同一个list。or []那个写法收藏了哈哈

duckling__sr
[链接]

不出声的雷最坑人 我之前也中过招 盯着屏幕半天没回过神

dear2001
[链接]

不报错也不吭声,这最让人头皮发麻。我那次也是卡了好久才回过神,踩过一次就长记性了。

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