Coding 的痕迹

一位互联网奔跑者的网上日记

0%

复制 Python 对象

帮同学写一个邻接表转邻接矩阵的程序,程序读入一个邻接表,节点编号从 1 至 1673,要求转成 1673 * 1673 的邻接表。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 邻接矩阵
# 索引范围 0 - 1672
matrix = [[0] * 1673] * 1673

# 读邻接表
with open('data.txt', 'r', encoding='utf-8') as f:
lines = f.readlines()
for line in lines:
# 输入数据中下标从 1 开始,1 - 1673,要减 1 处理
t = line.split()
start = int(t[0]) - 1
end = int(t[1]) - 1
matrix[start][end] = 1

# 打开输出文件
out_file = open('output2.csv', 'w+', encoding='utf-8')

# 写邻接矩阵
for line in matrix:
columns = [str(num) for num in line]
line_to_write = ','.join(columns) + '\n'

out_file.write(line_to_write)

运行后,程序所输出的邻接矩阵元素全为 1,不解。

找原因

单步调试发现,在读邻接表第一行后,有一列被置为了全 1。于是想到 Python 中通过乘法复制元素,在 Python 虚拟机层面只会复制对象的引用。

做个实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
In [9]: m = [[0] * 10] * 10

In [10]: m
Out[10]:
[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

In [11]: m[0][0] = 1

In [12]: m
Out[12]:
[[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]]

那么问题来了,对列表应用乘法,为什么 m[0][0] = 1 不会导致整个 m 中的元素变成 1 ?继续尝试:

1
2
3
4
5
6
7
8
9
In [13]: m = [0] * 10

In [14]: m
Out[14]: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

In [15]: m[0] = 1

In [16]: m
Out[16]: [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

很明显,[0] * 10 创建了一个长度为 10 的、元素全为 0 的列表,每个元素都是独立的。验证一下:

1
2
3
4
5
In [18]: m[0] is m[1]
Out[18]: True

In [19]: m[0][0] is m[0][1]
Out[19]: False

翻阅 Python 文档 3.1. 对象、值与类型,有一段关于可变与不可变类型的描述:

有些对象的 可以改变。值可以改变的对象被称为 可变的;值不可以改变的对象就被称为 不可变的。(一个不可变容器对象如果包含对可变对象的引用,当后者的值改变时,前者的值也会改变;但是该容器仍属于不可变对象,因为它所包含的对象集是不会改变的。因此,不可变并不严格等同于值不能改变,实际含义要更微妙。)
一个对象的可变性是由其类型决定的;例如,数字、字符串和元组是不可变的,而字典和列表是可变的。

创建对象时:

类型会影响对象行为的几乎所有方面。甚至对象编号的重要性也在某种程度上受到影响: 对于不可变类型,会得出新值的运算实际上会返回对相同类型和取值的任一现有对象的引用,而对于可变类型来说这是不允许的。例如在 a = 1; b = 1 之后,ab 可能会也可能不会指向同一个值为1的对象,这取决于具体实现,但是在 c = []; d = [] 之后,cd 保证会指向两个不同、单独的新建空列表。(请注意 c = d = [] 则是将同一个对象赋值给 cd。)

至此真相大白。

解决方案

可以使用列表生成式创建二维列表:

1
2
3
4
5
# Solution 1
matrix = [[0] * 1673 for _ in range(1673)]

# Solution 2
matrix = [[0 for _ in range(1673)] for _ in range(1673)]

类似操作也可使用 numpy 库更为简便:

1
2
3
import numpy as np

matrix = np.zeros((1673, 1673))

以上。

欢迎关注我的其它发布渠道