Python 踩坑之 append() 方法

简单介绍

Python 列表提供了一个 append() 方法用于在列表末尾追加新的对象。

append() 方法的语法为:

1
list.append(obj)

它接受一个参数 obj ,将其添加到 list 列表的末尾。没有返回值,但是会修改列表。

踩坑过程

距当初自学 Python 的时候已经过去很久了,由于没什么需求,这期间基本没再碰过这门语言,最近折腾深度学习才把 Python 重新拾起来。过了这么久,除了最基础的那部分其他知识基本已经忘的差不多了,这也是这次为什么会在这么基础的地方踩坑的原因。

场景描述

这个学期我们开了算法设计这门课程,所以就想着用 Python 来写几个算法的实现。

在写一个生成排列的算法时,遇到了下面这种场景:

  • 有一个空的列表 list 和一个 n 个元素的列表 nums
  • 算法会不断的改变 nums 内元素的排列顺序
  • 每次改变 nums 后都将其当时的状态记录到 list 中
  • 最后在 list 中存放的就是 nums 内元素的所有排列结果

对于这种场景,我采用的操作是在每次 nums 改变后,都使用 list.append(nums) 将其添加到 list 中保存下来。

正常情况下,最后 list 内的所有数组的元素排列顺序应当是不一样的。但实际输出的结果中 list 内的所有数组却都是一模一样的。

问题原因

在调试时发现问题就出在 list.append(nums) 这一句上,这一步确实将当前 nums 添加到了 list 内,但在随后过程中 nums 继续变化时, list 内刚刚添加的数组竟然也跟着发生了同样的变化。

经过查找资料,发现这个问题是由 append() 的实现方式导致的。

在 Python 中,对象赋值实际上是对象的引用。当你创建一个对象,然后把它赋给另一个变量的时候,Python 并没有拷贝这个对象,而只是拷贝了这个对象的引用。与之类似,列表类型的对象进行 append 操作时,实际上追加的也是该对象的引用,而不是直接拷贝该对象。

一个展示这种现象的示例如下(在 Python 交互式环境下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> l1 = [1,2,3]
>>> l2 = [3,2,1]
>>> l3 = [4,5,6]
>>> list = [l1,l2]
>>> list
[[1, 2, 3], [3, 2, 1]]
>>> list.append(l3)
>>> list
[[1, 2, 3], [3, 2, 1], [4, 5, 6]]
>>> l3.remove(4)
>>> l3
[5, 6]
>>> list
[[1, 2, 3], [3, 2, 1], [5, 6]]

可以看到,在删除 l3 内的一个元素后,我们没有对 list 做任何处理,但 list 内通过 append(l3) 添加的元素却同步发生了变化。

这就像 C/C++ 中用指针当参数时一样,这些元素虽然用了不同的命名来表示,但在内存中指向的却是同一个位置,一旦这块内存中存储的值发生变化,则所有命名方式表示的值都会同步变化。

解决方案

既然问题是由于 append() 添加的结果为原对象的引用导致的,那么我不直接 append 原对象,而是对原对象的每一个结果做个复制,然后再 append 这个复制体不就可以了吗?

所以在这个生成排列的算法中最终采用的解决办法为:将 list.append(nums) 修改为 list.append(nums.copy())