Python函数默认参数为何避免使用可变类型

周末不加班
周末不加班
发布时间:2026-10-15| 4人浏览过
原创

本文详解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),此处不再做详细测试。集合通常用于去重和关系运算,它属于无序、不重复且可变的类型。

以上为个人经验总结,希望能给大家提供一个参考,也希望大家多多支持本站。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。