Python函数默认参数为何避免使用可变类型
本文详解Python中函数默认参数使用可变类型(如列表、字典)导致的常见陷阱。当默认参数为可变对象时,其值在函数定义时即被计算并绑定到函数对象,多次调用会共享该状态,导致意外修改。文章通过代码示例对比不可变类型与可变类型的行为差异,并给出使用None作为默认值的标准解决方案,帮助开发者避免此类隐蔽Bug。
函数的默认参数请勿定义可变类型
在Python开发中,你经常可能会遇到一条关于默认参数的警告信息:
Default argument value is mutable
这条警告的核心含义是提醒开发者:在定义函数时,不要将可变类型的数据结构作为默认参数使用。
那么,为什么Python会发出这样的警告呢?这涉及到Python对参数求值的机制以及可变与不可变类型的区别。
可变类型和不可变类型
- 可变类型(mutable):列表,字典
- 不可变类型(unmutable):数字,字符串,元组
定义可变类型会有什么问题?
def fun(a=[]):
a.append(1)
print(a)
if __name__ == "__main__":
fun()
fun()
>>> [1]
[1, 1]如果在函数定义中使用了可变类型作为默认参数,你会发现一个奇怪的现象:在第二次乃至更多次调用同一个函数时,默认参数似乎“失去”了重置的效果,其状态会累积。
在需要重复调用同一个函数的业务场景中,这种特性非常容易导致难以察觉的逻辑错误。在调试(debug)阶段,你可能会发现明明没有传入参数,但函数内部的参数却已经有了值,进而执行了本不该执行的逻辑分支。
导致的原因
从我的理解来看:
我们在代码中定义的函数本身是一个function类的实例化对象。每当执行到函数定义语句时,Python就会创建一个function实例,而默认参数的值正是作为该实例的属性被存储下来的。
当调用函数且未传入具体参数,仅使用默认参数时,如果函数体内修改了该默认参数的值,这个被修改后的属性值会被永久保存在函数对象中。因此,当第二次调用同一个函数对象时,读取到的属性值已经是上一次修改后的结果,而非初始默认值。
type(fun)
>>> function解决方法
def fun(a=None):
if a is None:
a = []
a.append(1)
print(a)
if __name__ == "__main__":
fun()
fun()
>>> [1]
[1]关于可变类型作为默认参数时的注意点
请先观察以下代码,看看其输出结果是否与你预期的相符。
def e(v,l=[]):
l.append(v)
return l
l1=e(10)
l2=e(123,[])
l3=e("a")
print(l1,l2,l3)
# 输出: ([10, 'a'], [123], [10, 'a'])对于上述代码,官方标准解释是:带有默认参数的表达式在函数被定义的时候被计算一次,而不是在每次调用的时候重新计算。
用更通俗的话解释:当不传入默认值时,无论调用该函数多少次,函数体内部使用的始终是那个初始的默认列表“l”。由于这个“l”是可变类型,对其内容的修改会影响所有指向该内存地址的变量引用,例如代码中的l1和l3。
为了使上述观点更具说服力,我进行了以下几个测试。
测试:将可变类型列表换为字典
def e(k,v,d={}):
d[k]=v
return d
d1=e(10,10)
d2=e(123,123,{})
d3=e("a","a")
print(d1,d2,d3)
# 输出:({'a': 'a', 10: 10}, {123: 123}, {'a': 'a', 10: 10})测试:来个不可变类型字符串
def e(v,s=""):
s = s+v
return s
s1=e("我")
s2=e("a","")
s3=e("是")
print(s1,s2,s3)
# 输出: 我 a 是其实以上类型的测试结果已经说明了问题,但为了文章的完整性,我决定尝试用元组包裹列表,看看修改这个列表中的数据会产生什么结果。
实际上无需多言,最终打印出来的数据表现一定与“可变类型时的操作”类似。
为什么呢?因为元组本身并未被修改,我们修改的是元组内部包含的可变类型列表。
这里不再深入探讨深拷贝与浅拷贝的概念,以免偏离主题。
测试:元祖包个列表来
def e(v,t=([],)): # 传递有元素的元祖的时候要记得带逗号哦。
t[0].append(v)
# t = t[0].append(v) 要知道t[0].append(v)是没有返回值的,t会指向None,如果这样返回,外部打印的全部为None,所以不可以这样返回。
# 而且 如果你想 t[0]= t[0].append(v) 也是不行的,为啥?你在ipython中输入 dir(())你就知道了。
# 好吧,其实是因为元祖是可读不可写的。它能切片、遍历就已经很不错了。。。。。
return t
t1=e("我")
t2=e("a",([],))
t3=e("是")
print(t1,"\n",t2,"\n",t3)
# 输出:
# (['我', '是'],)
# (['a'],)
# (['我', '是'],)小结一下
由于家中停电,我转至网吧完成本文的撰写。本文使用的是Python 3的在线编辑器,部分代码命名或表述若有不当之处,敬请谅解。
关于集合(set),此处不再做详细测试。集合通常用于去重和关系运算,它属于无序、不重复且可变的类型。
以上为个人经验总结,希望能给大家提供一个参考,也希望大家多多支持本站。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









