Python函数的共享传参模式
Python 采用共享传参(call by sharing)的参数传递模式,这也是多数面向对象语言如 Ruby、Smalltalk 和 Java(Java 的引用类型)所采用的模式。在共享传参中,函数的形式参数获得实参中各个引用的副本,即函数内部的形参是实参的别名。
这种模式下,函数可能会修改作为参数传入的可变对象,但无法修改对象的标识。例如:
def f(a, b):
a += b
return a
x = 1
y = 2
f(x, y) # 返回 3
print(x, y) # 输出 (1, 2),数字 x 未改变
a = [1, 2]
b = [3, 4]
f(a, b) # 返回 [1, 2, 3, 4]
print(a, b) # 输出 ([1, 2, 3, 4], [3, 4]),列表 a 被改变
t = (10, 20)
u = (30, 40)
f(t, u) # 返回 (10, 20, 30, 40)
print(t, u) # 输出 ((10, 20), (30, 40)),元组 t 未改变
这表明数字和元组等不可变对象不会被函数修改,而列表等可变对象可能会被修改。
不要使用可变类型作为参数的默认值
Python 函数的可选参数可以有默认值,这是一个很棒的特性,但应避免使用可变对象作为参数的默认值。
以 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)
当实例化 HauntedBus 时,如果不传入乘客参数,多个实例会共享同一个默认列表。例如:
bus1 = HauntedBus(['Alice', 'Bill'])
bus1.pick('Charlie')
bus1.drop('Alice')
print(bus1.passengers) # 输出 ['Bill', 'Charlie']
bus2 = HauntedBus()
bus2.pick('Carrie')
bus3 = HauntedBus()
bus3.pick('Dave')
print(bus2.passengers) # 输出 ['Carrie', 'Dave']
print(bus2.passengers is bus3.passengers) # 输出 True
这是因为默认值在定义函数时计算,成为函数对象的属性。如果默认值是可变对象,修改它会影响后续的函数调用。通常应使用 None 作为接收可变值的参数的默认值。
防御可变参数
如果函数接收可变参数,要谨慎考虑调用方是否期望修改传入的参数。
以 TwilightBus 类为例:
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)
使用该类时会出现意外情况:
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = TwilightBus(basketball_team)
bus.drop('Tina')
bus.drop('Pat')
print(basketball_team) # 输出 ['Sue', 'Maya', 'Diana']
这违反了“最少惊讶原则”,因为学生从校车下车后,名字从篮球队名单中消失了。问题在于校车为传入的列表创建了别名。
正确的做法是在__init__ 中创建参数值的副本:
def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers)
这样处理既不会影响初始化时传入的参数,又更灵活,能接受各种可迭代对象作为参数。
总之,在类中直接把参数赋值给实例变量之前要谨慎,不确定时应创建副本,以减少客户的麻烦。
Python函数参数传递与可变参数处理
1480

被折叠的 条评论
为什么被折叠?



