python 中的小坑
背景
有一天,小红让小明帮忙编写一段将一个矩阵每个元素 x2 的 python 代码。小明大笔一挥:
小明轻松一测:
输出:
嗯,完美,没有问题,转手给小红发过去了。
小红把代码简单测了一下,没问题,小明果然靠谱。
结果呢,在实际运行过程中,时不时出现很离谱的操作。如:
结果输出:
离了个大谱,这什么鬼操作!
小红里面找到小明解释,小明盯着自己的代码看了好久,没问题呀,每个元素 x2,没毛病呀。
聪明的你发现问题了吗?
原因解释
我们再来看一下小明的代码:
乍一看,好像确实没问题,代码逻辑简单清晰,就是给每个元素 x2 再赋值回去。但是为什么出现上面这么奇怪的结果呢?
之所以出现这样的错误,是由于 python 的潜拷贝机制导致。
什么是 python 的浅拷贝呢?
在 Python 中,浅拷贝(shallow copy)指的是创建一个新对象,其内容是原始对象中对象的引用的拷贝,而不是对象本身。这意味着,如果原始对象是一个可变对象(如列表、字典等),那么原始对象和新对象将共享这些可变对象的底层数据。
示例
下面是一个浅拷贝的例子:
在这个例子中,shallow_copied_list
是original_list
的浅拷贝。当我们修改original_list
中的子列表时,shallow_copied_list
中的相应部分也会被修改,因为它们指向同一个子列表。
与深拷贝的区别
与浅拷贝相对的是深拷贝(deep copy),深拷贝会创建一个新对象,并且递归地复制所有对象,使得原始对象和新对象完全独立。深拷贝可以通过copy
模块的deepcopy()
函数实现。
浅拷贝和深拷贝的选择取决于你的具体需求,如果你需要完全独立的副本,应该使用深拷贝。如果你只是想复制对象的顶层结构,而不需要复制其内部的复杂结构,那么浅拷贝就足够了。
接下来我们回到小明的代码中:
在 Python 中,列表是可变对象,这意味着当你将一个列表赋值给另一个变量时,两个变量实际上指向内存中的同一个列表对象。在小红的代码中,列表 a
和 b
被赋值为相同的列表 [1, 1, 1]
,然后这两个列表被作为子列表放入到更大的列表 data
中。
这里,data
列表实际上看起来像这样:
注意,data
中的前两个元素实际上是指向同一个列表 [1, 1, 1]
的引用,而第三个元素是指向另一个列表 [1, 1, 1]
的引用。
当你调用 double(data)
函数时,你遍历 data
列表,并对其子列表中的每个元素乘以 2:
由于 data
的前两个子列表实际上是同一个列表,所以当函数修改这个列表时,data
中的前两个子列表都会受到影响。这就是为什么当你打印 data
时,你会看到 [4, 4, 4]
出现了两次。
然而,第三个子列表 b
指向的是另一个独立的列表 [1, 1, 1]
,所以它的值被翻倍为 [2, 2, 2]
,而没有被前两个子列表的修改所影响。
这就是为什么最终打印的结果是 [[4, 4, 4], [4, 4, 4], [2, 2, 2]]
。
规避建议
要编写一个正确的 double
函数,使得每个子列表都被正确地翻倍而不互相影响,你需要确保每个子列表都是独立的。有几种方法可以实现这一点:
方法 1:使用深拷贝
在将子列表添加到 data
列表之前,使用深拷贝来确保每个子列表都是独立的。
创建list
副本的方法有好多种,这里只是列举了其中一种。
方法 2:使用列表推导式创建新的子列表
在 double
函数内部,创建新的子列表,这样每个子列表都是独立的。
上面两种方法都可以达到效果,因此,当我们是小明(API 提供方)时,我们应该使用方法 2,确保我们的接口在任意参数下都可用。
当我们是小红(API 使用方)时,我们应该尽量使用方法 1,确保在double
函数未考虑到浅拷贝时,我们的程序依然可以得到正确的结果。
版权声明: 本文为 InfoQ 作者【SkyFire】的原创文章。
原文链接:【http://xie.infoq.cn/article/ea88e095a1b4b2f8928f53752】。文章转载请联系作者。
评论