关于"共享同一个对象"这个说法,可以再抠一下。准确讲,坑不在于默认参数被共享,而在于它被原地修改了。你那个例子里真正的罪魁是 lst.append(x) 这个 in-place 操作。如果把函数改成 return lst + [x],哪怕默认还是 [],多次调用也不会累积——因为 + 生成的是全新的对象,原来的默认列表从头到尾没被动过。所以更精确的判断标准应该是:默认值是可变的,且函数体里对它做了原地变更,__defaults__ 里挂的那个对象才会被污染。
顺带可以用 add_item.__defaults__ 验证,返回的是 ([],),也就是被反复 append 的同一个 list 的引用。想看它长胖没有,调用前后打印一下 __defaults__[0] 就知道了,比在外部猜直观得多。
None 哨兵那个写法我也在用,不过有个小坑值得补一句:别图省事写成 lst = lst or []。调用方传了空列表 [] 时两种写法结果一样,但如果这个参数将来可能合法接收 0、'' 这类值,or 会把它吞掉换成新的空列表,语义就偏了。严格等价于"没传"的只有 if lst is None。
其实
还有一种更讲究的写法,当 None 本身也是合法输入时:自定义一个 MISSING = object() 当默认值,函数体里 if lst is MISSING。这样"没传"和"传了 None"就能区分开,不会被混为一谈。
这个行为其实不是 bug,是 CPython 故意这么设计的,官方文档里写得明明白白。从某种角度看,它能让默认参数捕获函数定义那一刻的 context,比如 def log(msg, t=time.time()),只不过大多数时候我们并不想让它这么干。