使用可变对象作为python函数默认参数引发的问题

写python的都知道,python函数或者方法可以使用默认参数,比如

1 def foo(arg=None):
2     print(arg)
3 
4 foo()
5 
6 foo("hello world")

一个很简单的函数,参数arg默认使用None,当调用foo函数时,可以传入一个参数,也可以不传入参数,运行结果如下

1 None
2 hello world

这很好理解。默认参数是python一个很好的特性。

 

但是如果使用可变对象作为默认参数,就会引发问题。之前写过一个脚本,bug不断,后来终于找到了原因。下面引用Fluent python中的一个例子加以说明。

废话不多说,直接上代码

 1 """可变对象作为默认参数传入到函数引发的问题"""
 2 
 3 
 4 class Bus:
 5     def __init__(self, passengers=[]):
 6         self.passengers = passengers
 7 
 8     def pick(self, a_passenger):
 9         self.passengers.append(a_passenger)
10 
11     def drop(self, a_passenger):
12         self.passengers.remove(a_passenger)
13 
14 
15 if __name__ == '__main__':
16     bus1 = Bus(['Alice', 'Jeff', 'ethan'])
17     print(bus1.passengers)
18     bus1.pick("Simon")
19     print(bus1.passengers)  # 到此为止并没什么问题
20 
21     # 接下来问题来了, 创建了两个Bus对象,并进行操作。
22     bus2 = Bus()
23     print(bus2.passengers)
24     bus2.pick("Alice")   # "Alice"上了bus2
25     print(bus2.passengers)  # 打印bus2的乘客列表
26     bus3 = Bus()   # 创建另一个Bus实例,同样不传入参数(使用默认[]参数)
27     print(bus3.passengers)  # 打印bus3的乘客列表
28 
29     bus3.pick("alex")
30     print(bus3.passengers)
31     print(bus2.passengers)
32     print(bus3.passengers is bus2.passengers)
33 
34     print(bus3.passengers is bus1.passengers)

下面来看运行结果

['Alice', 'Jeff', 'ethan']
['Alice', 'Jeff', 'ethan', 'Simon']
[]
['Alice']
['Alice']
['Alice', 'alex']
['Alice', 'alex']
True
False

 

从结果可以看出,bus1这个对象似乎没什么问题,而bus2和bus3对象的passengers属性似乎是同一个。为什么会这样呢?

我们先来分析下Bus这个类

定义了一个Bus类,有一个__init__方法,pick方法和drop方法。__init__方法有一个默认参数,即passengers=[]。

大家应该都知道在python中列表对象([])是可变对象。 

当创建bus1对象的时候,传入了实实在在的passengers参数(一个非空列表)

当创建bus2和bus3对象的时候,没有传入参数,所以会使用默认参数([])

然后我对这几个Bus实例进行操作(调用pick或drop方法,即插入或者删除passengers列表中的乘客)

从结果可以很直观的看出来,bus1.passengers 并没什么问题,一切按照我们的预期进行。但是bus2的乘客"Alice"为什么会出现在bus3上? bus3的乘客“alex”为什么又会出现在bus2上???

 

其实这就是用可变对象作为默认参数引发的后果。因为当使用默认参数定义了一个函数/方法之后,加载模块的时候已经初始化了这个可变对象,所以当你调用这个函数/方法的时候,如果你未传入参数(即使用了默认参数), 则对函数/方法的不同调用,其实传入的是同一个可变对象(上面例子中是一个空列表)。 

下面请看

 1 class Bus:
 2     def __init__(self, passengers=[]):
 3         self.passengers = passengers
 4 
 5     def pick(self, a_passenger):
 6         self.passengers.append(a_passenger)
 7 
 8     def drop(self, a_passenger):
 9         self.passengers.remove(a_passenger)
10 
11 
12 if __name__ == '__main__':
13     print(Bus.__init__.__defaults__) 

 

运行结果

([],)

 

在这个例子中,并没有初始化Bus类, 而__init__方法的__defaults__属性,是一个空列表, 也就是说,在完成__init__方法定义的时候,就已经初始化了默认参数(一个空列表), 因为列表是可变对象,所以后续对Bus类的引用就会引发问题。

至此,问题原因很明确了,就是可变对象导致了这个问题,因此千万不要使用可变对象作为python函数/方法的默认参数, 一般建议使用None作为默认参数,即
1 def foo(arg=None):
2      pass

 




 

posted on 2018-05-06 22:30  hz_pythoner  阅读(127)  评论(0编辑  收藏  举报

导航