写点什么

python 中的小坑

作者:SkyFire
  • 2024-09-24
    陕西
  • 本文字数:2070 字

    阅读完需:约 7 分钟

背景

有一天,小红让小明帮忙编写一段将一个矩阵每个元素 x2 的 python 代码。小明大笔一挥:


def double(numbers):    for i in range(len(numbers)):        for j in range(len(numbers[i])):            numbers[i][j] *= 2
复制代码


小明轻松一测:


data = [    [1, 2, 3],    [4, 5, 6],    [7, 8, 9]]double(data)print(data)
复制代码


输出:


[[2, 4, 6], [8, 10, 12], [14, 16, 18]]
复制代码


嗯,完美,没有问题,转手给小红发过去了。


小红把代码简单测了一下,没问题,小明果然靠谱。


结果呢,在实际运行过程中,时不时出现很离谱的操作。如:


a = [1,1,1]b = [1,1,1]data = [a,a,b]double(data)print(data)
复制代码


结果输出:


[[4, 4, 4], [4, 4, 4], [2, 2, 2]]
复制代码


离了个大谱,这什么鬼操作!


小红里面找到小明解释,小明盯着自己的代码看了好久,没问题呀,每个元素 x2,没毛病呀。


聪明的你发现问题了吗?

原因解释

我们再来看一下小明的代码:


def double(numbers):    for i in range(len(numbers)):        for j in range(len(numbers[i])):            numbers[i][j] *= 2
复制代码


乍一看,好像确实没问题,代码逻辑简单清晰,就是给每个元素 x2 再赋值回去。但是为什么出现上面这么奇怪的结果呢?


之所以出现这样的错误,是由于 python 的潜拷贝机制导致。


什么是 python 的浅拷贝呢?


在 Python 中,浅拷贝(shallow copy)指的是创建一个新对象,其内容是原始对象中对象的引用的拷贝,而不是对象本身。这意味着,如果原始对象是一个可变对象(如列表、字典等),那么原始对象和新对象将共享这些可变对象的底层数据。

示例

下面是一个浅拷贝的例子:


original_list = [1, 2, [3, 4]]shallow_copied_list = original_list
# 修改原列表中的可变对象original_list[2].append(5)
print("Original List:", original_list) # 输出: [1, 2, [3, 4, 5]]print("Shallow Copied List:", shallow_copied_list) # 输出: [1, 2, [3, 4, 5]]
复制代码


在这个例子中,shallow_copied_listoriginal_list的浅拷贝。当我们修改original_list中的子列表时,shallow_copied_list中的相应部分也会被修改,因为它们指向同一个子列表。

与深拷贝的区别

与浅拷贝相对的是深拷贝(deep copy),深拷贝会创建一个新对象,并且递归地复制所有对象,使得原始对象和新对象完全独立。深拷贝可以通过copy模块的deepcopy()函数实现。


浅拷贝和深拷贝的选择取决于你的具体需求,如果你需要完全独立的副本,应该使用深拷贝。如果你只是想复制对象的顶层结构,而不需要复制其内部的复杂结构,那么浅拷贝就足够了。


接下来我们回到小明的代码中:


在 Python 中,列表是可变对象,这意味着当你将一个列表赋值给另一个变量时,两个变量实际上指向内存中的同一个列表对象。在小红的代码中,列表 ab 被赋值为相同的列表 [1, 1, 1],然后这两个列表被作为子列表放入到更大的列表 data 中。


a = [1, 1, 1]b = [1, 1, 1]data = [a, a, b]
复制代码


这里,data 列表实际上看起来像这样:


[[1, 1, 1], [1, 1, 1], [1, 1, 1]]
复制代码


注意,data 中的前两个元素实际上是指向同一个列表 [1, 1, 1] 的引用,而第三个元素是指向另一个列表 [1, 1, 1] 的引用。


当你调用 double(data) 函数时,你遍历 data 列表,并对其子列表中的每个元素乘以 2:


def double(numbers):    for i in range(len(numbers)):        for j in range(len(numbers[i])):            numbers[i][j] *= 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 列表之前,使用深拷贝来确保每个子列表都是独立的。


import copy
def double(numbers): for sublist in numbers: for i in range(len(sublist)): sublist[i] *= 2
a = [1, 1, 1]b = [1, 1, 1]data = [copy.deepcopy(a), copy.deepcopy(a), copy.deepcopy(b)]double(data)print(data)
复制代码


创建list副本的方法有好多种,这里只是列举了其中一种。

方法 2:使用列表推导式创建新的子列表

double 函数内部,创建新的子列表,这样每个子列表都是独立的。


def double(numbers):    for i in range(len(numbers)):        numbers[i] = [x * 2 for x in numbers[i]]
a = [1, 1, 1]b = [1, 1, 1]data = [a, a, b]double(data)print(data)
复制代码


上面两种方法都可以达到效果,因此,当我们是小明(API 提供方)时,我们应该使用方法 2,确保我们的接口在任意参数下都可用。


当我们是小红(API 使用方)时,我们应该尽量使用方法 1,确保在double函数未考虑到浅拷贝时,我们的程序依然可以得到正确的结果。

发布于: 刚刚阅读数: 7
用户头像

SkyFire

关注

这个cpper很懒,什么都没留下 2018-10-13 加入

会一点点cpp的苦逼码农

评论

发布
暂无评论
python中的小坑_Python_SkyFire_InfoQ写作社区