得之我幸 失之我命

when someone abandons you,it is him that gets loss because he lost someone who truly loves him but you just lost one who doesn’t love you.

Python 深拷贝

写 python 的过程中,拷贝总是少不了的,遥记得 C++ 里有个东西叫深拷贝(总之涉及到指针就变得啥都不好理解了),同为面向对象,但是 python 不是没有指针吗?

这还得从 python 的变量开始说起

变量和对象

在 Python 中,一个变量可以说是内存中的一个对象的「标签」,它绑定到内存中的对象。准确地说,变量不是引用另一个变量,而是绑定到某个对象

一般情况下,当一个对象不再能通过程序中的任何方式访问时,它就可能被垃圾回收机制回收。不过,像小整数、短字符串这类对象,Python 可能会缓存复用,不一定会立即释放。

1
2
3
4
5
6
7
8
9
10
11
12
a = 2  # 现在变量 a 绑定到内存中的一个 int 型对象(a 相当于 2 的名字)
b = a # 把变量 a 赋给 b,不是让 b 引用变量 a,而是让 b 也绑定到 a 当前绑定的对象,相当于给当前内存中对象(此处的 2)增加一个名字

print(b)
2

a = 3 # 给 a 重新赋值,此时 a 绑定到内存中的一个 int 型对象(a 现在是 3 的名字),原来的值为 2 的 int 型对象不能再通过 a 去访问它
print(a)
3

print(b) # `b = a` 的含义不是「b 引用 a 这个变量」,而是「b 也绑定到 a 当前绑定的那个对象”,所以当 `a` 重新绑定到其他对象,`b` 不会跟着改变
2

综上,变量不需要类型,类型是属于对象的,这也是 Python 中的变量可以被任何类型赋值的原因

Everything is Object in Python?

Python 使用对象模型来储存数据,任何类型的值都是一个对象

所有的 Python 对象都有 3 个特征:

  1. 身份:每一个对象都有自己的唯一的标识,可以使用内建函数 id() 来得到它。这个值可以被认为是该对象的内存地址
  2. 类型:对象的类型决定了该对象可以保存的什么类型的值,可以进行什么操作,以及遵循什么样的规则。type() 来查看 python 对象的类型
  3. :对象表示的数据项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
a = 1
print(id(a))
140720048772880

b = 2
print(id(b))
140720048772912

c = a
print(id(c))
140720048772880

# 运算符 is、is not 就是通过 id() 的返回值(即身份)来判定的,也就是看它们是不是同一个对象的「标签」
print(c is a)
True

print(c is b)
False

print(a is not b)
True

可变对象与不可变对象

Python 3 中有 6 种标准的数据类型:Numbers、Strings、List、Tuple、Sets、Dictionaries,其中 Numbers、Strings 和 Tuple 是不可更改的对象,而 List、Dictionaries、Sets 是可以修改的对象,那么可变与不可变有什么区别呢?

这里需要先区分两个概念(根据 id() 是否还一样):

  • 重新绑定变量名:让变量名绑定到另一个对象,对象身份发生变化。例如 a = 5a = [1, 2]
  • 原地修改对象:变量名仍然绑定到原来的对象,对象身份不变,但对象内部的值或内容发生变化。例如 a.append(7)a[0] = 100

只有当多个变量指向同一个可变对象,并且其中一个变量对这个对象做了原地修改时,其他变量才会看到变化

不可变对象

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
30
31
x = 1  # x 绑定到内存中值为 1 的对象
a = x # a 也绑定到内存中值为 1 的对象
print(a is x)
True

a = 5 # 因为 int 对象不可改变,于是创建一个新的 int 对象(值为 5)并且把 a 绑定到它
print(a)
5

print(a is x)
False

print(x)
1 # 而 x 仍然绑定原来的值为 1 的 int 对象,


x = 1 # x 绑定到内存中值为 1 的对象
a = x # a 也绑定到内存中值为 1 的对象
print(a is x)
True

b = 5 # b 绑定到内存中值为 5 的对象
a = b # a 也绑定到内存中值为 5 的对象
print(a)
5

print(a is x)
False

print(x) # 而 x 仍然绑定原来的值为 1 的 int 对象,所以 x 没有改变
1

可变对象

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
30
31
x = [1, 2]  # x 绑定到一个 list 类型的对象
a = x # a 也绑定到同一个 list 类型的对象
print(a is x)
True

a.append(7) # 因为 list 对象是可以改变的,append 在其末尾添加了一个元素,list 对象的内容发生了改变
print(a)
[1, 2, 7]

print(a is x) # 但是 x 和 a 仍然是绑定到同一个 list 对象
True

print(x) # 所以变量 x 的内容发生了改变
[1, 2, 7]


# 但是需要注意,列表是可变对象,不代表只要出现列表,外部变量就一定会改变。如果在函数中只是重新绑定形参,不会影响外部变量
x = [1, 2] # x 绑定到一个 list 类型的对象
a = x # a 也绑定到同一个 list 类型的对象
print(a is x)
True

a = [7, 8, 9] # a 绑定到一个新的 list 对象
print(a)
[7, 8, 9]

print(a is x)
False

print(x) # x 仍然绑定到最初的那个 list 类型的对象,所以 x 不变
[1, 2]

再看一个容易混淆的例子:

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
30
31
a = [1, 2, 3]
b = a
print(a is b)
True

a = a + [4] # a + [4] 会创建一个新的 list 对象,然后 a = ... 让变量 a 重新绑定到这个新对象
print(a)
[1, 2, 3, 4]

print(a is b)
False

print(b) # 而 b 仍然绑定原来的 list
[1, 2, 3]


# 而对于 list 来说,+= 通常会原地修改原来的 list:
a = [1, 2, 3]
b = a
print(a is b)
True

a += [4] # 在原 list 上追加元素
print(a)
[1, 2, 3, 4]

print(a is b)
True

print(b) # 所以 a 和 b 都能看到变化
[1, 2, 3, 4]

注意,+= 是否原地修改取决于对象类型。对于字符串、整数、元组这类不可变对象,+= 不能原地修改,只能创建新对象再重新绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = "abc"
b = a
print(a is b)
True

a += "d"
print(a)
abcd

print(a is b)
False

print(b)
abc

深、浅拷贝

在深拷贝之前,得先知道什么是浅拷贝

浅拷贝是指创建一个新的对象,其内容是原对象中元素的引用(拷贝组合对象,不递归拷贝子对象)

常见的浅拷贝有:切片操作、工厂函数、对象的 copy() 方法、copy 模块中的 copy 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = [1, 2, 3]
b = list(a)
print(id(a))
1997304139008

print(id(b))
1997304191296
for i in range(len(a)):
... print(id(a[i]), id(b[i]))
...
140720048772880 140720048772880
140720048772912 140720048772912
140720048772944 140720048772944
# a 浅拷贝得到 b,a 和 b 指向内存中不同的 list 对象,但它们的元素却指向相同的 int 对象

深拷贝是指创建一个新的对象,然后递归地拷贝原对象所包含的子对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
a = [1, 2, 3]
import copy
c = copy.deepcopy(a)
print(id(a))
1997304139008

print(id(c))
1997304138880
for i in range(len(a)):
... print(id(a[i]), id(c[i]))
...
140720048772880 140720048772880
140720048772912 140720048772912
140720048772944 140720048772944 # 怎么还是一样的?

为什么使用了深拷贝,a 和 c 中元素的 id 还是一样呢?

因为对于不可变对象,当需要一个新的对象时,Python 可能不会真的创建一个全新的对象,而是复用已经存在的、类型和值都一致的对象。这个机制不会影响 ac 的相互独立性,因为不可变对象不能被原地修改。即使两个列表中的元素是同一个不可变对象,也不会出现“修改其中一个列表的元素对象,另一个列表跟着变”的问题。

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
30
31
32
import copy
a = [[1, 2],[5, 6], [8, 9]]
b = copy.copy(a) # a 浅拷贝得到 b
c = copy.deepcopy(a) # a 深拷贝得到 c
print(id(a), id(b))
1997304191360 1997304139008 # a 和 b 不同

for i in range(len(a)):
... print(id(a[i]), id(b[i]))
...
1997304138816 1997304138816 # a 和 b 的子对象相同
1997304193344 1997304193344
1997304191936 1997304191936

print(id(a), id(c))
139832578518984 139832578622456 # a 和 c 不同

for i in range(len(a)):
... print(id(a[i]), id(c[i]))
...
1997304138816 1997304270464
1997304193344 1997304269568
1997304191936 1997304270272 # a 和 c 的子对象不同

a[0][0] = 7
print(a)
[[7, 2], [5, 6], [8, 9]]

print(b)
[[7, 2], [5, 6], [8, 9]]
print(c)
[[1, 2], [5, 6], [8, 9]]

此时,对于可变对象而言,浅拷贝带来的潜在隐患就一目了然了

be yourself, everyone else is already taken.