Python/Numpy中的副作用陷阱?分享恐怖故事和惊险逃脱经历
我在考虑从Matlab转到Python/numpy来进行数据分析和数值模拟。我已经使用Matlab(还有SML-NJ)很多年了,对这种没有副作用(除了输入输出)的功能环境非常熟悉,但对Python中的副作用有点犹豫。有没有人能分享一下他们在处理副作用时遇到的常见问题,以及他们是如何解决这些问题的?举个例子,当我在Python中尝试以下代码时,我有点惊讶:
lofls = [[]] * 4 #an accident waiting to happen!
lofls[0].append(7) #not what I was expecting...
print lofls #gives [[7], [7], [7], [7]]
#instead, I should have done this (I think)
lofls = [[] for x in range(4)]
lofls[0].append(7) #only appends to the first list
print lofls #gives [[7], [], [], []]
提前谢谢大家
2 个回答
我最近又碰到了这个问题,(在使用了多年的Python后)当时我想去掉对numpy的一个小依赖。
如果你是从matlab过来的话,应该使用并信任numpy
的函数来处理单一类型的数组。再加上matplotlib,这两个包可以让你更顺利地过渡。
import numpy as np
np.zeros((4,)) # to make an array full of zeros [0,0,0,0]
np.zeros((4,1)) # another one full of zeros but 2 dimensions [[0],[0],[0],[0]]
np.zeros((4,0)) # an empty array like [[],[],[],[]]
np.zeros((0,4)) # another empty array, which can not be represented with python lists o_O
等等。
把同一个可变对象的引用和不同对象的引用搞混,确实是个“坑”,这是所有非函数式编程语言都会遇到的问题,尤其是那些有可变对象和引用的语言。初学者在写Python代码时,常常会犯一个错误,就是错误使用了一个可变的默认值,比如:
def addone(item, alist=[]):
alist.append(item)
return alist
这段代码如果目的是让addone
保持自己的状态(并把这个不断增长的列表返回给后续调用者),那它是正确的;但如果程序员错误地认为每次调用都会创建一个新的空列表,那就不对了。
刚接触编程的初学者,尤其是习惯于函数式编程语言的人,可能会对Python内置容器中的命令查询分离这一设计感到困惑:那些没有特别返回值的修改方法(也就是大多数的修改方法)不会返回任何东西(具体来说,它们返回None
)——它们的工作都是“就地”完成的。因为误解这一点而产生的错误很容易被发现,比如:
alist = alist.append(item)
这几乎可以肯定是个错误——它把一个项目添加到了名为alist
的列表中,但随后又把alist
重新绑定到了None
(即append
调用的返回值)。
我提到的第一个问题是关于早绑定的,可能会误导那些认为绑定是晚绑定的人;而有些问题则正好相反,有些人的期望是早绑定,但实际上是晚绑定。举个例子(假设有一个图形用户界面框架...):
for i in range(10):
Button(text="Button #%s" % i,
click=lambda: say("I'm #%s!" % i))
这段代码会显示十个按钮,上面写着“Button #0”、“Button #1”等等,但当你点击每一个按钮时,它们都会说
自己是#9
——因为lambda
中的i
是晚绑定的(通过词法闭包)。解决这个问题的方法是利用参数的默认值是早绑定的这一点(就像我提到的第一个问题一样!),把最后一行改成:
click=lambda i=i: say("I'm #%s!" % i))
现在lambda
中的i
是一个有默认值的参数,而不再是一个自由变量(通过词法闭包查找),所以代码就能按预期工作了(当然还有其他解决方法)。