Python 可变默认参数陷阱:为什么 def f(lst=[]) 会记住上次的值
Python 可变默认参数陷阱:为什么 def f(lst[]) 会记住上次的值这是 Python 面试和 code review 里的常客,也是新手最难以置信的一个坑:一个函数带了默认参数lst[],你以为每次调用都是空列表,结果它把上一次的数据「记」下来了。defadd_item(item,lst[]):lst.append(item)returnlstprint(add_item(1))# [1]print(add_item(2))# [1, 2] ← 什么鬼?不应该是 [2] 吗print(add_item(3))# [1, 2, 3]第二次调用没传lst,按直觉应该拿到一个全新的空列表,结果却带着上次的1。这篇讲清楚原理和正确写法。根源:默认值只在定义时求值一次Python 里,函数的默认参数值是在def这行被执行时(也就是函数定义时)计算一次,然后绑定到函数对象上,而不是每次调用时重新创建。也就是说,lst[]里的那个[]在整个程序里只被创建一次。之后每次调用不传lst,用的都是同一个列表对象。你对它append,改的就是这个共享对象,下次调用自然还在。可以亲眼验证——默认值存在函数的__defaults__里:defadd_item(item,lst[]):lst.append(item)returnlstprint(add_item.__defaults__)# ([],)add_item(1)print(add_item.__defaults__)# ([1],) ← 默认值本身被改了!add_item(2)print(add_item.__defaults__)# ([1, 2],)看到没,__defaults__里那个列表在被不断修改。默认参数不是「每次调用的初始值」,而是「函数身上挂着的一个固定对象」。为什么 Python 要这样设计不是 bug,是有意为之。默认值在定义时求值,好处是简单、可预测、性能好(不用每次调用都重算)。对不可变类型(int、str、tuple、None)完全没问题,因为你没法「原地修改」它们:defgreet(name,greeting你好):returnf{greeting},{name}# greeting 是字符串,不可变,共享同一个对象也没关系坑只出现在默认值是可变对象(list、dict、set)且函数内部修改了它的时候。正确写法:用 None 做哨兵标准解法是把默认值设为None,在函数体内判断后再创建新对象:defadd_item(item,lstNone):iflstisNone:lst[]# 每次调用都新建,互不干扰lst.append(item)returnlstprint(add_item(1))# [1]print(add_item(2))# [2] ← 正确了print(add_item(3))# [3]None是不可变单例,做默认值绝对安全;用is None判断后在函数内新建,保证每次调用拿到独立对象。dict、set 同理:defbuild_config(overridesNone):ifoverridesisNone:overrides{}base{timeout:30,retries:3}base.update(overrides)returnbase为什么用is None而不是if not lst?因为not lst会把「传进来的空列表[]」也判成 True,从而误替换掉调用方故意传的空容器。is None只在「真的没传」时触发,语义精确。一个更隐蔽的变种:默认值调函数同样的规则也坑「用函数调用做默认值」的场景,常见于想给时间戳一个默认值:fromdatetimeimportdatetimedeflog(msg,tsdatetime.now()):# 错!now() 只在定义时执行一次print(f[{ts}]{msg})log(启动)# 隔几秒log(结束)# 两条日志的时间戳一模一样——都是模块导入那一刻datetime.now()在def执行时就算出了一个固定时间,之后每次调用都用这个「化石时间」。正确写法还是哨兵:deflog(msg,tsNone):iftsisNone:tsdatetime.now()# 每次调用时才取当前时间print(f[{ts}]{msg})什么时候可以「故意」利用它极少数情况下,这个「记住上次」的特性被拿来做函数级缓存/计数器,但这属于 hack,可读性差,不推荐在业务代码里用:defcounter(_cache{n:0}):_cache[n]1return_cache[n]print(counter())# 1print(counter())# 2要做缓存,请用functools.lru_cache;要做计数,用类或闭包。别靠可变默认参数,会让读代码的人一脸问号。怎么提前发现这个坑好消息是静态检查工具能直接揪出来。用pylint会报W0102: dangerous-default-value;ruff里对应规则B006(来自 flake8-bugbear):# ruff 一秒扫出可变默认参数ruff check--selectB006 your_module.py# your_module.py:1:20: B006 Do not use mutable data structures for argument defaultsCI 里挂上这条规则,基本能杜绝这个问题流入代码库。小结函数默认参数在定义时求值一次,并作为同一个对象绑定到函数上,不是每次调用新建。对list/dict/set这类可变默认值且函数内会修改它时,就会「记住上次」——本质是多次调用共享了同一个对象。正确写法:默认值设None,函数体内if x is None: x []再新建;用is None而非if not x,避免误伤传入的空容器。用函数调用(如datetime.now())做默认值是同一个坑的变种,同样用哨兵解决。上线前用ruff --select B006或pylint自动拦截。一句话记忆:默认值只算一次;可变对象做默认值,就用None哨兵 函数内新建。