函数的参数作为引用

  • 不要使用可变类型作为参数的默认值
  • 防御可变参数


Python唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言都采用这一模式,包括Ruby、Smalltalk和Java。

共享传参指函数的哥哥形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。

函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。

示例1 有个简单的函数,它再参数上调用 += 运算符。分别把数字、列表和元组传给那个函数,实际传入的实参会以不同的方式收到影响。

#示例1:函数可能会修改接收到的任何可变对象
>>> 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))

➊ 数字x没变。
➋ 列表a变了。
➌ 元组t没变。

不要使用可变类型作为参数的默认值

可选参数可以有默认值,这是Python函数定义的一个很棒的特性,然而,我们应该避免使用可变的对象作为参数的默认值

一个简单的类,说明可变默认值的危险
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)

➊ 如果没传入passengers参数,使用默认绑定的列表对象,一开始是空列表。
➋ 这个赋值语句把self.passengers变成passengers的别名,而没有传入passengers参数时,后者又是默认列表的别名。
➌ 在self.passengers上调用.remove()和.append()方法时,修改的其实是默认列表,它是函数对象的一个属性。

#备受幽灵乘客折磨的校车
>>> bus1 = HauntedBus(['Alice', 'Bill']) 
>>> bus1.passengers 
['Alice', 'Bill'] 
>>> bus1.pick('Charlie') 
>>> bus1.drop('Alice') 
>>> bus1.passengers  #➊ 
['Bill', 'Charlie'] 
>>> bus2 = HauntedBus()  #➋ 
>>> bus2.pick('Carrie') 
>>> bus2.passengers 
['Carrie'] 
>>> bus3 = HauntedBus()  #➌ 
>>> bus3.passengers  #➍ 
['Carrie'] 
>>> bus3.pick('Dave') 
>>> bus2.passengers  #➎ 
['Carrie', 'Dave'] 
>>> bus2.passengers is bus3.passengers  #➏ 
True 
>>> bus1.passengers  #➐ 
['Bill', 'Charlie']

➊ 目前没什么问题,bus1没有出现异常。
➋ 一开始,bus2是空的,因此把默认的空列表赋值给self.passengers。
➌ bus3一开始也是空的,因此还是赋值默认的列表。
➍ 但是默认列表不为空!
➎ 登上bus3的Dave出现在bus2中。
➏ 问题是,bus2.passengers和bus3.passengers指代同一个列表。
➐ 但bus1.passengers是不同的列表。

问题在于,没有指定初始乘客的HauntedBus实例会共享同一个乘客列表。这种问题很难发现。

实例化HauntedBus时,如果传入乘客,会按预期运作。但是不为HauntedBus指定乘客的话,奇怪的事就发生了,这是因为self.passengers变成了passengers参数默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响

可以审查HauntedBus.__init__对象,看看它的__defaults__属性中的那些幽灵学生:

>>> dir(HauntedBus.__init__)  # doctest: +ELLIPSIS 
['__annotations__', '__call__', ..., '__defaults__', ...] 
>>> HauntedBus.__init__.__defaults__ 
(['Carrie', 'Dave'],)

最后,我们可以验证bus2.passengers是一个别名,它绑定到HauntedBus.init.__defaults__属性的第一个元素上:

>>> HauntedBus.__init__.__defaults__[0] is bus2.passengers 
True

可变默认值导致的这个问题说明了为什么通常使用None作为接收可变值的参数的默认值
__init__方法检查passengers参数的值是不是None,如果是就把一个新的空列表赋值给self.passengers。

防御可变参数

如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。

最后一个校车示例中,TwilightBus实例与客户共享乘客列表,这会产生意料之外的结果。在分析实现之前,我们先从客户的角度看看TwilightBus类是如何工作的。

#示例3:从TwilightBus下车后,乘客消失了
>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']  #➊ 
>>> bus = TwilightBus(basketball_team)  #➋ 
>>> bus.drop('Tina')  #➌ 
>>> bus.drop('Pat') 
>>> basketball_team  #➍ 
['Sue', 'Maya', 'Diana']

➊ basketball_team中有5个学生的名字。
➋ 使用这队学生实例化TwilightBus。
➌ 一个学生从bus下车了,接着又有一个学生下车了。
➍ 下车的学生从篮球队中消失了!

TwilightBus违反了设计接口的最佳实践,即“最少惊讶原则”。学生从校车中下车后,她的名字就从篮球队的名单中消失了,这确实让人惊讶。

示例4是TwilightBus的实现,随后解释了出现这个问题的原因。

#示例3:一个简单的类,说明接受可变参数的风险
class TwilightBus: 
    """让乘客销声匿迹的校车""" 
 
    def __init__(self, passengers=None): 
        if passengers is None: 
            self.passengers = []  #➊ 
        else: 
            self.passengers = passengers  #➋ 
 
    def pick(self, name): 
        self.passengers.append(name) 
 
    def drop(self, name): 
        self.passengers.remove(name)  #➌

➊ 这里谨慎处理,当passengers为None时,创建一个新的空列表。
➋ 然而,这个赋值语句把self.passengers变成passengers的别名,而后者是传给__init__方法的实参(即示例8-14中的basketball_team)的别名。
➌ 在self.passengers上调用.remove()和.append()方法其实会修改传给构造方法的那个列表。

这里的问题是,校车为传给构造方法的列表创建了别名

正确的做法是:校车自己维护乘客列表。

修正的方法很简单:在__init__中,传入passengers参数时,应该把参数值的副本赋值给self.passengers

def __init__(self, passengers=None): 
    if passengers is None: 
        self.passengers = [] 
    else: 
        self.passengers = list(passengers)  #➊

➊ 创建passengers列表的副本;如果不是列表,就把它转换成列表。

内部像这样处理乘客列表,就不会影响初始化校车时传入的参数了。此外,这种处理方式还更灵活:现在,传给passengers参数的值可以是元组或任何其他可迭代对象,例如set对象,甚至数据库查询结果,因为list构造方法接受任何可迭代对象。自己创建并管理列表可以确保支持所需的.remove()和.append()操作,这样.pick()和.drop()方法才能正常运作。