20 分钟
Python 进阶

可变默认参数陷阱

Python 最著名的坑:def f(x=[]) 到底发生了什么

  • 理解默认参数在函数定义时求值
  • 识别可变默认参数陷阱
  • 掌握正确的 None 惯用法
  • 理解 None 作为哨兵值的用法

可变默认参数陷阱

这是 Python 面试中最常考的题,也是新手最容易踩的坑。真正理解它,你就避开了一个绝大多数新手都会反复栽跟头的陷阱。

陷阱重现

示例代码(可运行)
🚫核心原理

Python 的默认参数值在函数定义时(def 语句执行时)求值一次,之后每次调用都复用同一个对象。如果默认值是可变对象(list/dict/set),多次调用之间会共享状态。

正确写法

示例代码(可运行)
🐍资深工程师经验谈

这个设计不是 bug,是 Python 的有意为之。默认参数在定义时求值有一个好处:可以实现缓存/memoization。但绝大多数情况下你不想要这个行为。记住这个惯用法:def f(x=None): if x is None: x = []。这是 Python 社区的标准写法,所有 linter 都会警告 mutable default argument。

哪些类型安全?

示例代码(可运行)
CPython源码

默认参数的底层实现

函数对象有一个 __defaults__ 属性,存储默认参数的元组:

示例代码(可运行)

这就是默认参数在定义时求值的原因——它们就挂在函数对象上,和函数同生命周期。

找 Bug默认参数 target=[] 在函数定义时只创建一次,两次调用共享同一个列表,所以 result2 是 [1, 2] 而不是 [2]。
def append_to(element, target=[]): target.append(element) return target result1 = append_to(1) result2 = append_to(2) print(result2)

资深工程师加餐

底层原理 · 大厂视角 · 工程经验,点卡片展开

内层函数(或 lambda)捕获的是外层变量本身,而不是定义那一刻的值;循环里定义的多个函数,循环结束后调用往往都拿到最后的值。解决办法是用默认参数 f(x=x) 立即绑定,或用工厂函数包一层。配合 LEGB 作用域顺序(局部→外层→全局→内建)理解,90% 的作用域谜题都能解开。

挑战任务

修复默认参数

简单+50 XP

修复下面的函数,每次调用不传 cache 时,就用独立的字典。

修复默认参数
2 个测试用例

课后作业

安全的累加器

中等+20 XP

写一个函数,每次调用返回累加值,不传参数时从0开始。

安全的累加器
1 个测试用例