背景
在函数或类定义中传入的参数是可变参数,常见的是字典、列表、数组(ndarray),函数内容如果仅仅是引用该这些对象没有什么大问题。但是如果涉及增、删操作,将会发生非常诡异的事情。
下面以《流畅的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 = ['Alice', 'Bill', 'Carrie', "Dave"]
bus1 = HauntedBus(passengers)
bus1.pick("Charlie")
bus1.drop("Alice") # Alice下车
print(f"bus1.passengers={bus1.passengers}") #
print(f"passengers={passengers}")
"""
bus1.passengers=['Bill', 'Carrie', 'Dave', 'Charlie']
passengers=['Bill', 'Carrie', 'Dave', 'Charlie']
"""
类HauntedBus违反了涉及接口的最佳实践,即“最少惊讶原则”。学生从校车中下车后,他的名字就从篮球队消失了。
简单解释就是:实例的修改不能影响其它外部变量。
解决问题
通过python内置的数据结构可知,列表、字典是可变的数据对象.而且赋值是浅拷贝,所在在该模式下修改赋值后的变量也会影响源变量。针对上述问题解决方案就是将对应的变量进行copy。
class HauntedBus:
def __init__(self, passengers=[]):
self.passengers = passengers.copy()
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
passengers = ['Alice', 'Bill', 'Carrie', "Dave"]
bus1 = HauntedBus(passengers)
bus1.pick("Charlie")
bus1.drop("Alice") # Alice下车
print(f"bus1.passengers={bus1.passengers}") #
print(f"passengers={passengers}")
"""
bus1.passengers=['Bill', 'Carrie', 'Dave', 'Charlie']
passengers=['Alice', 'Bill', 'Carrie', 'Dave']
"""
然而copy并不总是有效,如果当前这个数据对象非常大的情况下,copy将比较耗时,此时比较便捷的方式就是基于目标数据结构进行输出数据重构。
这种情况在输入的参数是字典时(且嵌套层级较多时),尤为明显。
ndarray中类似的问题
import numpy as np
a = np.arange(10)
def func(a):
"""测试,将a的前2个值修改为0"""
a[:2] = 0
return True
print(f"before a={a}")
func(a)
print(f"after a={a}")
"""
before a=[0 1 2 3 4 5 6 7 8 9]
after a=[0 0 2 3 4 5 6 7 8 9]
"""
def func2(a):
"""测试,将a的前2个值修改为0"""
b=a.copy()
b[:2] = 0
print(f"b={b}")
return True
c=np.arange(10)
print(f"before c={c}")
func2(c)
print(f"after c={c}")
"""
before c=[0 1 2 3 4 5 6 7 8 9]
b=[0 0 2 3 4 5 6 7 8 9]
after c=[0 1 2 3 4 5 6 7 8 9]
"""
小结
- 输入的参数如果是可变对象,若涉及对象修改问题需慎重考虑:
- 单通道串行:正常操作即可
- 多通道并行(存在多个对象同时或部分同时应用的情况):
- 若数据体量较小,可以采用深拷贝或浅拷贝的方式
- 若数据体量较大,且浅拷贝不足以解决问题时,但一般情况不建议深拷贝。通常基于目标数据格式进行重组。