警惕函数入参为可变对象
可变对象作为函数入参
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 来作为参数的默认值,避免出现上述的情况。