Python 函数中使用默认值参数 — 谈谈可变对象的坑?!

在 python 中定义函数,其参数可以使用多种不同的方式,其中包括 “默认值参数”类型,那么当作默认值的对象有什么限制和要求么?这里搞不好还真有坑! 接下来我们主要从两个角度来谈谈。

 

参数的默认值:

  • 使用可变对象
  • 使用不可变对象

 

 

默认参数使用可变对象会怎样?

 

先复原需求

定义一个函数,为传入的列表(list)尾部添加一个“end”元素。

如:传入: [1, 2, 3]

        输出: [1 ,2, 3, 'end']

 

实现代码:

def addend(lt=[]):
    lt.append('end')
    return lt

# 传入一个普通的列表,输出结果正确
lst = [1, 2, 3, 4]
print(addend(lst))

# 不传入任何参数,第二次调用,多输出了一个'end'
print(addend())
print(addend())

---------------------
输出:
[1, 2, 3, 4, 'end']
['end']
['end', 'end']

 

问题分析

 

先观察这个函数:

  • 其参数使用了默认参数的定义方式
  • 默认参数是一个空白的列表(列表是一个可变的对象 —— 这是重点)

 

再看执行过程:

   1.  def addend(lt=[])

  • 在定义函数(addend)的时候,为其默认参数先分配了一块空间,用于存储可变对象[](即一个空白的列表),我们可以理解为 lt 这个形参变量,就像一个指针,指向了这块存储空间。

 

    2. lst = [1,2,3,4]

  • 当在外部定义一个列表(lst)时,也会分配一块存储空间,空间内存储了列表内容([1,2,3,4]),可以看做是由 lst 这个变量指向了该空间。

 

    3. print(addend(lst))

  • 当调用函数并传入实参(lst)的时候,并不是把lst列表中的内容拷贝到lt列表中,而是使lt指向的对象发生了改变,使lt也指向了lst指向的空间,这样,lt 和 lst 都指向了相同的存储空间([1,2,3,4])。
  • 在函数内部,将lt指向的对象空间内添加一个新的元素“end”,最终输出[1,2,3,4,'end']。

 

    4. print(lst)

  • 由上面分析可知, lst 和 lt 都指向了相同的存储空间,所以 lst 指向空间内容输出也为 [1,2,3,4,'end']

 

Python 函数中使用默认值参数 — 谈谈可变对象的坑?!_第1张图片

 

 

 

稍加变化

 

清楚了以上机制后,我们再稍微变化一下,函数定义处依旧使用默认参数形式,但是其默认的参数值不是一个空的列表了,而是有数据元素的列表([1,2])。

 

同样的道理,当调用这个参数的时候,只要传入了实参列表,不论形参值是什么,形参变量 lt 都指向了新传入的实参空间,并在新传入的空间内附加上 'end' 元素。

 

所以输出的结果和形参的值没有任何关系。

 

def addend(lt=[1, 2]):
    lt.append('end')
    return lt


lst = [1, 2, 3, 4]
print(addend(lst))
print(lst)

------------------------------
输出:
[1, 2, 3, 4, 'end']
[1, 2, 3, 4, 'end']

 

 

 

 

接下来继续分析不传入实参的情况

  • 如果不传入实参,形参变量 lt 将不会转移其指向的存储空间
  • 每调用一次该函数,都向 lt 指向的空间内添加一个 'end' 元素
  • 第一次调用,由于 lt 指向空间内没有任何内容,因此输出一个 'end',符合预期
  • 第二次调用,lt 指向空间内已经有了一个 'end'元素,此时又添加了一个 'end' ,因此会输出两个'end'元素,就与我们的预期不一致了。
def addend(lt=[]):
    lt.append('end')
    return lt


print(addend())
print(addend())

---------------------
输出:
['end']
['end', 'end']

 

Python 函数中使用默认值参数 — 谈谈可变对象的坑?!_第2张图片

 

 

 

稍加改变:

  • 函数定义处依旧使用默认参数形式,但是其默认的参数值不是一个空的列表了,而是有数据元素的列表([1,2])。
  • 连续两次调用该函数,均不传入实参。
  • 从输出结果可以清楚的看到,在原参数值列表中每次调用都添加了一个 'end'元素

 

def addend(lt=[1, 2]):
    lt.append('end')
    return lt


print(addend())
print(addend())

-------------------------
输出:
[1, 2, 'end']
[1, 2, 'end', 'end']

 

 

PyCharm 的提示:

 

当函数定义中的默认参数赋值为可变对象的时候,PyCharm会自动检测并加以提示,如下所示:

 

点击“more...” 链接,查看更多详细的描述。

 

有道词典走起:

Default argument value is mutable less... (Ctrl+F1)

默认参数值是可变的

This inspection detects when a mutable value as list or dictionary is detected in a default value for an argument. Default argument values are evaluated only once at function definition time, which means that modifying the default value of the argument will affect all subsequent calls of the function.

该检查检测何时在参数的默认值中检测到列表或字典等可变值。默认参数值只在函数定义时计算一次,这意味着修改参数的默认值将影响函数的所有后续调用。

 

 

 

如果函数默认参数使用不可变对象又会怎样呢?

 

说起不可变对象,首当其冲会想到元组(tuple),把它放到默认参数中试试吧:

  • 调用函数时,不提供任何实参。
  • 代码运行直接报错:“tuple 对象没有 append 属性”,即不能向其添加元素。
def addend(lt=(1, 2)):
    lt.append('end')
    return lt


print(addend())
print(addend())

---------------------------
输出:
Traceback (most recent call last):
  File "D:/A00__Dev/pyprojects/untitled1/l2.py", line 10, in 
    print(addend())
  File "D:/A00__Dev/pyprojects/untitled1/l2.py", line 2, in addend
    lt.append('end')
AttributeError: 'tuple' object has no attribute 'append'

 

分析错误原因,依然离不开前面我们得出的结论:

  • 由于没有传入实参,lt指向的存储空间一直没有发生变化
  • 但是这个空间是受控的,相当于只读的,不允许向里面添加任何内容
  • 此时执行添加 'end'操作,当然不允许了

Python 函数中使用默认值参数 — 谈谈可变对象的坑?!_第3张图片

 

 

 

 

 

综上,在定义函数默认值参数的时候,其默认值尽量不要使用可变对象,为了防止产生类似问题,做的更彻底些,默认参数值可以直接使用单例的空对象 None 来代替,然后在函数体中判断调用时是否传入了空的参数。

def addend(lt=None):
    if lt is None:
        lt = []
    lt.append('end')
    return lt


print(addend())
print(addend())

 

 

 

你可能感兴趣的:(Python)