Python为何不能用可变对象作为默认参数的值

yipeiwu_com6年前Python基础

先来看一道题目:

>>> def func(numbers=[], num=1):
... numbers.append(num)
... return numbers
>>> func()
[1]
>>> func()
[1, 1]
>>> func()
[1, 1, 1]

我们似乎发现了一个Bug,每次用相同的方式调用函数 func() 时,返回结果竟然不一样,而且每次返回的列表在不断地变长。

>>> id(func())
4330472840
>>> id(func())
4330472840

从上面可以看出,函数的返回值其实是同一个列表对象,因为他们的id值是一样的,只不过是列表中的元素在变化。为什么会这样呢?

这要从函数的特性说起,在 Python 中,函数是第一类对象(function is the first class object),换而言之,函数也是对象,跟整数、字符串一样可以赋值给变量、当做参数传递、还可以作为返回值。函数也有自己的属性,比如函数的名字、函数的默认参数列表。

# 函数的名字
>>> func.__name__ 
'func'
# 函数的默认参数列表
>>> func.__defaults__ 
([1, 1, 1, 1, 1], 1)

def是一条可执行语句,Python 解释器执行 def 语句时,就会在内存中就创建了一个函数对象(此时,函数里面的代码逻辑并不会执行,因为还没调用嘛),在全局命名空间,有一个函数名(变量叫 func)会指向该函数对象,记住,至始至终,不管该函数调用多少次,函数对象只有一个,就是function object,不会因为调用多次而出现多个函数对象。

函数对象生成之后,它的属性:名字和默认参数列表都将初始化完成。

初始化完成时,属性 __default__ 中的第一个默认参数 numbers 指向一个空列表。

当函数第一次被调用时,就是第一次执行 func()时,开始执行函数里面的逻辑代码(此时函数不再需要初始化了),代码逻辑就是往numbers中添加一个值为1的元素

第二次调用 func(),继续往numbers中添加一个元素

第三次、四次依此类推。

所以现在你应该明白为什么调用同一个函数,返回值确每次都不一样了吧。因为他们共享的是同一个列表(numbers)对象,只是每调用一次就往该列表中增加了一个元素

如果我们显示地指定 numbers 参数,结果截然不同。

>>> func(numbers=[10, 11])
[10, 11, 1]

因为numbers被重新赋值了,它不再指向原来初始化时的那个列表了,而是指向了我们传递过去的那个新列表对象,因此返回值变成了 [10, 11, 1]

那么我们应该如何避免前面那种情况发生呢?就是不要用可变对象作为参数的默认值。

正确方式:

>>> def func(numbers=None, num=1):
... if numbers is None:
... numbers = [num]
... else:
... numbers.append(num)
... return numbers
...
>>> func()
[1]
>>> func()
[1]
>>> func()
[1]

如果调用时没有指定参数,那么调用方法时,默认参数 numbers 每次都被重新赋值了,所以,每次调用的时候numbers都将指向一个新的对象。这就是与前者的区别所在。

那么,是不是说我们永远都不应该用可变对象来作为参数的默认值了吗?并不是,既然Python有这样的语法,就一定有他的应用场景,就像 for ... else 语法一样。我们可以用可变对象来做缓存功能。

例如:计算一个数的阶乘时可以用一个可变对象的字典当作缓存值来实现缓存,缓存中保存计算好的值,第二次调用的时候就无需重复计算,直接从缓存中拿。

def factorial(num, cache={}):
if num == 0:
return 1
if num not in cache:
print('xxx')
cache[num] = factorial(num - 1) * num
return cache[num]
print(factorial(4))
print("-------")
print(factorial(4))

输出:

---第一次调用---
xxx
xxx
xxx
xxx
24
---第二次调用---
24

第二次调用的时候,直接从 cache 中拿了值,所以,你说用可变对象作为默认值是 Python 的缺陷吗?也并不是,对吧!你还是当作一种特性来使用。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持【听图阁-专注于Python设计】。

相关文章

Django后台获取前端post上传的文件方法

Django后台获取前端post上传的文件方法

由于初学Django所以对于其特有的表单模板和models中的filefiled并不是特别熟悉。因此在本次开发中还是依照以往自己在html中写好图片提交表单: 注意在<form&...

django模板语法学习之include示例详解

前言 在很多网站中,基本上的都会有一个开头和一个结尾,在每一个网页中都会显示。相对于这种的来说,在Django中,最好的方法就是使用include的标签,在每一个模板中都加入这个开头和结...

用Django实现一个可运行的区块链应用

用Django实现一个可运行的区块链应用

对数字货币的崛起感到新奇的我们,并且想知道其背后的技术——区块链是怎样实现的。   但是完全搞懂区块链并非易事,我喜欢在实践中学习,通过写代码来学习技术会掌握得更牢固。通过构建...

使用Python中的线程进行网络编程的入门教程

引言 对于 Python 来说,并不缺少并发选项,其标准库中包括了对线程、进程和异步 I/O 的支持。在许多情况下,通过创建诸如异步、线程和子进程之类的高层模块,Python 简化了各种...

python 字符串和整数的转换方法

数字转成字符串 方法一: 使用格式化字符串: tt=322 tem='%d' %tt tem即为tt转换成的字符串 常用的格式化字符串: %d 整数 %f%F 浮点数 %e%E...