python函数默认值不能用可变对象,因默认值在def执行时绑定一次,后续调用共享同一对象;正确做法是用none作占位符并在函数内初始化,类型提示用optional[list[str]]并辅以文档说明。

为什么 Python 函数参数默认值不能用可变对象
因为 None 是不可变的单例,而列表、字典等可变对象在函数定义时就被实例化一次,后续所有调用会共享同一份内存。这不是 bug,是 Python 的求值时机决定的——默认值在 def 执行时就计算并绑定,不是每次调用时才生成。
常见错误现象:def append_to(items=[], item="x"): items.append(item); return items 连续调用会不断累积元素,第二次调用返回 ["x", "x"],第三次是 ["x", "x", "x"]。
- 正确做法:把默认值设为
None,在函数体内显式初始化:if items is None: items = [] - 不要写
if not items:,空列表也会进分支,逻辑错乱 -
None在这里只是“占位符”,不参与业务逻辑,语义清晰且安全
None 作为默认值时如何做类型提示
类型检查器(如 mypy)需要知道你实际期望的类型,不能只写 Optional[List[str]] 就完事——它只表示“可能是 None 或列表”,但没说明“传 None 时函数内部会自动创建新列表”。
使用场景:写库函数或公共 API 时,既要运行时行为明确,也要静态类型可推导。
立即学习“Python免费学习笔记(深入)”;
- 标注类型用
Optional[List[str]],但文档或注释里必须写明“None表示使用默认空列表” - 如果想更严谨,可以用
union[List[str], None],效果等价,但Optional更常用 - 别漏掉运行时判断:
items = items if items is not None else [],类型提示不会帮你做这事
什么情况下不该用 None 做默认值
当 None 本身是合法输入,且和“未提供”需要区分时,就不能用它当哨兵值。
例如一个缓存函数 get_user(id, cache=None):如果 cache 是 None 表示“不用缓存”,那你就没法表达“用户没传 cache 参数”这个意图。
- 这时该用自定义哨兵对象:
_sentinel = Object(),再写def get_user(id, cache=_sentinel): ... - 判断用
cache is _sentinel,绝对安全,因为object()实例不可能被外部构造出来 - 别用字符串比如
"__default__",容易被误传,也破坏类型一致性
None 默认值对性能和兼容性的影响
几乎没有运行时开销:None 是单例,比较 is 极快;初始化逻辑(如 [] 或 {})只在真正需要时执行,避免了无谓的对象创建。
兼容性方面,这是 Python 社区二十年形成的隐式约定,几乎所有标准库和主流包都遵循这套模式(比如 dict.get(key, default=None))。
- 第三方工具(如 Pydantic、fastapi)都依赖这个约定解析可选参数
- 如果你用其他值(比如
0、"")作默认,可能和业务数据冲突,导致条件判断出错 - Python 3.12+ 的
typing.Required和typing.NotRequired也不改变这一实践,它们面向的是 TypedDict 字段,不是函数参数
真正容易被忽略的是:哨兵值必须和业务语义解耦。哪怕你再确定“业务里绝不会传 None”,只要接口契约允许它作为有效输入,就得换方案——因为调用方不归你管。