avatar


数据类型、内存结构、拷贝和深拷贝(Python)

前半部分更多的是铺垫,意料之外 情理之中在于文章最后一个"混乱"的现象

栈内存和堆内存

首先,来看一段代码。

示例代码:

1
2
3
4
5
6
7
t1 = 1
t2 = t1

t1 = 11

print(t1)
print(t2)

运行结果:

1
2
11
1

这段代码的运行结果没有任何问题。
现在,我们把t1改成一个列表,并修改列表的元素。

示例代码:

1
2
3
4
5
6
7
t1 = [1, 2, 3]
t2 = t1

t1[0] = 11

print(t1)
print(t2)

运行结果:

1
2
[11, 2, 3]
[11, 2, 3]

我们只修改了t1,但是打印的t2感觉也被修改了。

没意思

首先,执行t1 = [1, 2, 3]t2 = t1,此时内存结构如图所示。

内存结构图

t1t2指向堆内存中的同一块区域。所以,执行t1[0] = 11,修改堆内存中的第一个元素为11,会导致t1t2都受影响。

不可变数据类型和可变数据类型

实际上,在Python的内存结构中,除了栈内存和堆内存,还有一块区域,“常量区”,本文的内存结构有省略。

在Java中,有"栈内存"和"堆内存",基本数据类型在"栈内存"中;引用数据类型,在"栈内存"中存储的是地址,指向"堆内存"中的区域。
本文分析的是Python代码,在这里我们和Java做比较。

Python中有"不可变数据类型"和"可变数据类型"。

  • “不可变数据类型"对应Java中的"基本数据类型”,有:int(整型)、float(浮点型)、str(字符串)、bool(布尔型)。
  • “可变数据类型"对应Java中的"引用数据类型”,有:list(列表)、set(集合)、dic(字典)和tuple(元组)。

tuple(元组),其实属于"不可变数据类型",但将其比拟为"引用数据类型"更好。

我们可以看看如下代码的运行,"引用数据类型"的id和"基本数据类型"的id长度都不一样,而且t3元祖的id,看起来更像是"引用数据类型"的长度。

示例代码:

1
2
3
4
5
6
7
8
9
t1 = [1, 2, 3]
t2 = t1
t3 = (1, 2, 3)

print(id(t1))
print(id(t2))
print(id(1))
print(id(t1[0]))
print(id(t3))

运行结果:

1
2
3
4
5
140168006292928
140168006292928
4498938560
4498938560
140167940343040

那么,为什么称元祖是"不可变数据类型"呢?
因为,确实 不可变

示例代码:

1
2
3
4
5
6
7
8
l = [1, 2, 3]
t = (1, 2, 3)

l[0] = 11
print(l)

t[0] = 11
print(t)

运行结果:

1
2
[11, 2, 3]
TypeError: 'tuple' object does not support item assignment

但是,我们可以修改元祖中的 "可变数据类型"的内容
示例代码:

1
2
3
4
t = ([1, 2, 3], 1, 2, 3)

t[0][0] = 11
print(t)

运行结果:

1
([11, 2, 3], 1, 2, 3)

注意,是 "可变数据类型"的内容,如下的代码就会报错。
示例代码:

1
2
3
4
t = ([1, 2, 3], 1, 2, 3)

t[0] = 11
print(t)

运行结果:

1
TypeError: 'tuple' object does not support item assignment

关于Python中的数据类型,在《Python、JavaScript、Git和ffmpeg:5.Python [1/2]》中,有更多讨论。

copy

那么,如果想修改t1的同时,不影响t2呢?
简单,调用copy()方法。

示例代码:

1
2
3
4
5
6
7
8
9
10
t1 = [1, 2, 3]
t2 = t1.copy()

print(id(t1))
print(id(t2))

t1[0] = 11

print(t1)
print(t2)

运行结果:

1
2
3
4
140686581646720
140686581468416
[11, 2, 3]
[1, 2, 3]

我们看到t2 = t1.copy(),这时候t2并没有和t1指向同一个地址,此时的内存结构如图。

copy内存结构图

所以,t1[0] = 11,不会影响t2

deepcopy

再来看一个例子,这回写的是t2 = t1.copy()

示例代码:

1
2
3
4
5
6
7
8
9
10
t1 = [1, 2, 3, ['a', 'b']]
t2 = t1.copy()

print(id(t1))
print(id(t2))

t1[-1][0] = 'aaa'

print(t1)
print(t2)

运行结果:

1
2
3
4
140361615183104
140361614692160
[1, 2, 3, ['aaa', 'b']]
[1, 2, 3, ['aaa', 'b']]

这更不好玩了

因为t1 = [1, 2, 3, ['a', 'b']],最后一个元素是"引用数据类型"。
此时的内存结构如下图。

deepcopy

我们还可以打印id看一下。

示例代码:

1
2
3
4
5
6
7
8
t1 = [1, 2, 3, ['a', 'b']]
t2 = t1.copy()

print(id(t1))
print(id(t2))

print(id(t1[-1]))
print(id(t2[-1]))

运行结果:

1
2
3
4
140384734186496
140384733687680
140384734365312
140384734365312

那么,怎么解决呢?
这就需要一个工具包了,copy,以及这个工具包的一个方法,deepcopy()

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import copy

t1 = [1, 2, 3, ['a', 'b']]
t2 = copy.deepcopy(t1)

print(id(t1))
print(id(t2))

print(id(t1[-1]))
print(id(t2[-1]))

print(t1)
print(t2)
t1[-1][0] = 'aaa'
print(t1)
print(t2)

运行结果:

1
2
3
4
5
6
7
8
140267864099840
140267863608960
140267864278848
140267862635520
[1, 2, 3, ['a', 'b']]
[1, 2, 3, ['a', 'b']]
[1, 2, 3, ['aaa', 'b']]
[1, 2, 3, ['a', 'b']]

一个"混乱"的现象

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import copy

t = [1, 2, 3, ['a', 'b', 'c'], {}]
tt = t
t_copy = t.copy()
t_deepcopy = copy.deepcopy(t)


print(id(t))
print(id(tt))
print(id(t_copy))
print(id(t_deepcopy))
print()
print(id(t[3]))
print(id(tt[3]))
print(id(t_copy[3]))
print(id(t_deepcopy[3]))
print()
print(id(t[-1]))
print(id(tt[-1]))
print(id(t_copy[-1]))
print(id(t_deepcopy[-1]))

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
140298566224256
140298566224256
140298565733504
140298564759936

140298566407680
140298566407680
140298566407680
140298566222400

140298555008064
140298555008064
140298555008064
140298555008448

这个是没有任何问题的。

  • tt = t,所以id(t)id(tt)是一样的,指向堆内存中的同一块区域。
  • t_copy = t.copy(),所以id(t)id(t_copy)不一样,但是id(t[3])id(t_copy[3])是一样的,因为index为3的是"引用数据类型",id(t[-1])id(t_copy[-1])同理。
  • t_deepcopy = copy.deepcopy(t),所以id(t)id(t_deepcopy)不一样,而且id(t[3])id(t_deepcopy[3])也不一样的,id(t[-1])id(t_deepcopy[-1])同理。

接下来,我们修改t[3][0] = 'aaa'

示例代码:

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
import copy

t = [1, 2, 3, ['a', 'b', 'c'], {}]
tt = t
t_copy = t.copy()
t_deepcopy = copy.deepcopy(t)

print(t)
print(tt)
print(t_copy)
print(t_deepcopy)
print(id(t[3]))
print(id(tt[3]))
print(id(t_copy[3]))
print(id(t_deepcopy[3]))

t[3][0] = 'aaa'

print()
print(t)
print(tt)
print(t_copy)
print(t_deepcopy)
print(id(t[3]))
print(id(tt[3]))
print(id(t_copy[3]))
print(id(t_deepcopy[3]))

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[1, 2, 3, ['a', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]
140194765948672
140194765948672
140194765948672
140194765767680

[1, 2, 3, ['aaa', 'b', 'c'], {}]
[1, 2, 3, ['aaa', 'b', 'c'], {}]
[1, 2, 3, ['aaa', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]
140194765948672
140194765948672
140194765948672
140194765767680

这一步也没有任何问题,除了t_deepcopy,其他的都受影响。

接下来,就是见证奇迹的时刻了。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import copy

t = [1, 2, 3, ['a', 'b', 'c'], {}]
tt = t
t_copy = t.copy()
t_deepcopy = copy.deepcopy(t)

print(t)
print(tt)
print(t_copy)
print(t_deepcopy)

t[-1] = {'k': 'v'}

print()
print(t)
print(tt)
print(t_copy)
print(t_deepcopy)

运行结果:

1
2
3
4
5
6
7
8
9
[1, 2, 3, ['a', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]

[1, 2, 3, ['a', 'b', 'c'], {'k': 'v'}]
[1, 2, 3, ['a', 'b', 'c'], {'k': 'v'}]
[1, 2, 3, ['a', 'b', 'c'], {}]
[1, 2, 3, ['a', 'b', 'c'], {}]

理论上,应该t_copy也应该受影响啊!t_copy也应该打印[1, 2, 3, ['a', 'b', 'c'], {'k': 'v'}]才对啊!

问题的根源出在哪里?
t[-1] = {'k': 'v'}

我们来分析一下。
首先

1
2
3
4
t = [1, 2, 3, ['a', 'b', 'c'], {}]
tt = t
t_copy = t.copy()
t_deepcopy = copy.deepcopy(t)

这时候的内存结构如图所示。
内存结构

然后执行t[-1] = {'k': 'v'}
这一段在干嘛?
在修改最后一个元素的指向。
但,只有ttt受影响了。

内存结构

这就是为什么ttt[1, 2, 3, ['a', 'b', 'c'], {'k': 'v'}],而t_copyt_deepcopy[1, 2, 3, ['a', 'b', 'c'], {}]

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/19903
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Kaka Wan Yifan

留言板