警惕函数入参为可变对象

可变对象作为函数入参

Python 唯一支持的函数传递模式是共享传参。这样会导致,函数内可以修改可变对象来影响到对象在外部的使用。但是由于无法改变对象的标识,所以 += 运算符在除了不可变对象外是不会影响到对象内容的。

下面的例子展示了传入数字、列表、元组后的效果:

>>> def f(a, b):
...     a+= b
...     return a
...
 
>>> x = 1
>>> y = 2
>>> f(x, y)
3
>>> x, y
(1, 2)
 
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b
([1, 2, 3, 4], [3, 4])
 
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t, u ➌
((10, 20), (30, 40))

防御可变参数

如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数,毕竟修改传入的参数,会影响到外部的使用。

这个没有什么说明,就写代码的时候明确好。

使用 None 而不是可变对象作为参数默认值

下面的例子,HauntedBus 类的构造方法使用空列表作为参数默认值。然而,这会导致如果创建对象时都使用参数默认值,那么类的多个实例对象会共享这个空列表对象。如下所示:

class HauntedBus:
    """备受幽灵乘客折磨的校车"""
	def __init__(self, passengers=[]):
	    self.passengers = passengers
	def pick(self, name):
	    self.passengers.append(name)
	def drop(self, name):
	    self.passengers.remove(name)
 
>>> bus2 = HauntedBus()
>>> bus2.pick('Carrie')
>>> bus3 = HauntedBus()
>>> bus3.pick('Dave')
>>> bus2.passengers
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers
True

这种情况的原因在于,定义函数时,会将参数的默认值变成函数对象的属性。调用函数(如例子中的构造方法)时,会保留函数对象的属性。

参数的默认值是可变对象,因此函数属性中存储的是这个对象的引用。基于默认值调用函数,就会使用这个对象的引用,从而共享这个可变对象。

>>> dir(HauntedBus.__init__)  # doctest:+ELLIPSIS
['__annotations__', '__call__', ..., '__defaults__', ...]
>>> HauntedBus.__init__.__defaults__    # 此时,已经被上面例子中的操作修改了
(['Carrie', 'Dave'],)
>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers
True

所以,如果参数是可变对象,那么就使用 None 来作为参数的默认值,避免出现上述的情况。