在平常运维工作中,难免会有一些修改服务配置文件的操作,为了安全和可以回滚起见,我们习惯性的会将源配置文件做一个拷贝,这样以来即便配置文件参数被修改错了也没事,可以快速从备份的副本还原回来。
同样,在 Python 中如果涉及到数据传递,在传递过程中就有可能会发生数据被修改的问题,为了防止数据被修改,就需要生成一个副本,这就产生了拷贝
对象
在 Python 中,一切皆对象。任何数据类型、函数、模块在 Python 中都被当做对象处理。
所有的 Python 对象都有三个属性:身份、类型、值
1
2
3
4
5
6
7name = 'Tom'
id(name) # ID其实是内存地址,即身份的唯一标识
19018976
type(name) # 对象的类型,决定对象可以引用什么样的值
>>> name # 对象引用的值,即实际表示的数据
'Tom'
可变与不可变对象
可变对象:对象的值可变,身份是不变的,例如列表、字典、集合,这些都是容器类型的数据,容器的内存地址是不变的,发生变化的是容器内部的数据
不可变对象: 对象的值和身份都是不变的,新创建的对象被原来的变量名引用,旧的对象就会被丢弃,最后交给垃圾回收机制处理掉,例如数字、字符串、元组
变量赋值
或许在很多人的直观印象中,变量是一个容器,给变量赋值,就像是往一个存储的容器中填入一个数据,再次赋值就是把容器中的数据换掉,在 Python 中,这种理解是不准确的!
更恰当的比喻是,我们可以把变量当做是一个标签:给变量赋值就是把标签贴在一个物体上;再次赋值就是把标签取下来贴在另一个物体上。
对象引用
在 Python 程序中,每个对象都会在内存中申请开辟一块空间来保存该对象,该对象在内存中所在位置的地址被称为引用。在开发程序时,所定义的变量名实际就是对象的地址引用。
引用实际就是内存中的一个数字地址编号(内存地址),在使用对象时,只要知道这个对象的内存地址,就可以操作这个对象,但是因为内存地址不方便在开发时使用和记忆,所以使用变量名的形式来代替对象的内存地址。 在 Python 中,变量就是地址的一种表示形式,并不开辟存储空间。
就像在访问网站时我们输入的是域名,而不是IP,但实际都是通过 IP 地址来进行网络通信的,而 IP 地址不方便记忆,所以使用域名来代替 IP 地址,在使用域名访问网站时,域名被 DNS 服务器解析成 IP 地址来使用。
在创建一个数据项时,将其赋值给一个变量,或者将其插入到一个组合中时,实际上是使得某个变量对内存中存放数据的对象进行引用,使用 del 时,实际上是删除了相应的对象引用
不可变对象的引用赋值1
2
3
4
5
6
7
8
9
10
11
12a = 1
id(a)
497079344
>>>b = a
>>>id(b)
497079344
>>>a = 'xyz'
>>>id(a)
19019808
>>>id(b)
497079344
>>>
a = 1 相当于把标签 a 贴在了 1 上,他的身份(内存地址)是 497079344
使用 b = a 的方式,实际上就相当于把标签 b 也贴在了标签 a 贴的那个物体上,也就是 a 和 b 都指向了同一块内存地址,可以简单的认为给对象的引用做了一个别名
当重新为 a 进行赋值时(a='xyz'),实际上是在内存中新开辟了一段空间存放字符串对象 'xyz' 并把 a 的指向改为 'xyz',而 b 则还是原来的对象引用
可变对象的引用赋值
可变对象保存的其实是对象引用(内存地址),而不是真正的对象数据。当对可变对象进行赋值时,只是将可变对象中保存的引用指向了新的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15>>>a = [1, 2, 3]
>>>b = a
>>>id(a)
19007448
>>>id(b)
19007448
>>>a[0] = 'hello'
>>>a
['hello', 2, 3]
>>>b
['hello', 2, 3]
>>>id(a)
19007448
>>>id(b)
19007448
上例中变量 a 并不是引用了实际数据 1, 2, 3 的内存地址,而是单纯的列表的内存地址,引用实际数据的是这个列表对象而不是变量 a
当改变列表中的数据时,改变的不是列表的内存地址,而改变的是列表中对实际数据的引用
浅拷贝
不可变对象的拷贝1
2
3
4
5
6
7
8
9
10
11
12>>>import copy
>>>a = 1
>>>b = copy.copy(a)
>>>id(a)
505009200
>>>id(b)
505009200
>>>a
1
>>>b
1
>>>
不可变对象的拷贝就是对象赋值,实际上是让多个对象同时指向一个引用 (内存地址)
可变对象的拷贝1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16>>>import copy
>>>a = [1, 2, 3]
>>>b = copy.copy(a)
>>>id(a)
11778728
>>>id(b)
11778968
>>>a[0] = 'hello'
>>>id(a)
11778728
>>>id(b)
11778968
>>>a
['hello', 2, 3]
>>>b
[1, 2, 3]
可变对象的拷贝,会在内存中开辟一个新的空间来保存拷贝的数据。当再改变之前的对象时,对拷贝之后的对象没有任何影响
可变对象与不可变对象的组合1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24>>>import copy
>>>a = [1, 2, 3, ['Tom', 'Jerry']]
>>>b = copy.copy(a)
>>>id(a)
11779648
>>>id(b)
11778728
>>>id(a[3])
11779728
>>>id(b[3])
11779728
>>>a[3][0] = 'Dog'
>>>id(a)
11779648
>>>id(b)
11778728
>>>id(a[3])
11779728
>>>id(b[3])
11779728
>>>a
[1, 2, 3, ['Dog', 'Jerry']]
>>>b
[1, 2, 3, ['Dog', 'Jerry']]
复杂对象在拷贝时并没有解决数据在传递之后数据改变的问题。 这是因为 copy() 函数在拷贝对象时,只是将指定对象中的所有引用拷贝了一份,如果这些引用当中包含了一个可变对象的话,那么数据还是会被改变。 这种拷贝方式,称为浅拷贝。
浅拷贝只拷贝顶层数据,不拷贝子层数据
再比如
1
2
3
4
5
6>>>d1=dict.fromkeys(['host1','host2','host3'],['test1','test2'])
>>>d1
{'host1': ['test1', 'test2'], 'host2': ['test1', 'test2'], 'host3': ['test1', 'test2']}
>>>d1['host2'][1]='test3'
>>>d1
{'host1': ['test1', 'test3'], 'host2': ['test1', 'test3'], 'host3': ['test1', 'test3']}
深拷贝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>>>import copy
>>>a = [1, 2, 3, ['Tom', 'Jerry']]
>>>b = copy.deepcopy(a)
>>>id(a)
11778968
>>>id(b)
11779528
>>>id(a[3])
11779648
>>>id(b[3])
11779688
>>>a[3][0] = 'Dog'
>>>id(a)
11778968
>>>id(b)
11779528
>>>id(a[3])
11779648
>>>id(b[3])
11779688
>>>a
[1, 2, 3, ['Dog', 'Jerry']]
>>>b
[1, 2, 3, ['Tom', 'Jerry']]
>>>
深拷贝会逐层进行拷贝,直到拷贝的所有引用都是不可变引用为止
总结在 Python 中,默认的拷贝方式是浅拷贝。从时间上看,浅拷贝花费时间更少;从使用空间上看,浅拷贝花费内存更少;从执行效率上看,浅拷贝只拷贝顶层数据,一般情况下比深拷贝效率高
不可变对象在赋值时会开辟新空间
深、浅拷贝对不可变对象拷贝时,不开辟新空间,相当于赋值操作
浅拷贝在拷贝时,只拷贝第一层中的引用,如果元素是可变对象,并且被修改,那么拷贝的对象也会发生变化
大多数情况下,编写程序时,都是使用浅拷贝,除非有特定的需求