python 深拷贝和浅拷贝之可变和不可变对象总结

时间:2021-09-20 19:53:42

了解深拷贝和浅拷贝之前先要理解可变与不可变对象

python只允许使用引用传递,有可变对象和不可变对象,可变对象:list,dict.
不可变对象有:int,string,float,tuple

Python int,string,float,tuple不可变
举栗子:

  def init_object():
i
= 89
j
= 89
print(id(89))
print('i id:'+str(id(i)))
print('j id:'+str(id(j)))
print(i is j)
j
= j + 1
print('new i id:' + str(id(i)))
print('new j id:' + str(id(j)))
print(i is j)

if __name__ == '__main__':
init_object()

 

有i和j俩个变量的值为89,通过打印89的ID和变量i,j在内存中的id我们得知它们都是指向同一块
内存。所以说i和j都是指向同一个对象的。然后我们修改j的值,让j的值+1.按道理j修改之后应该i
的值也发生改变的,因为它们都是指向的同一块内存,但结果是并没有。因为int类型是不可变类型,
所有其实是j复制了一份到新的内存地址然后+1,然后j又指向了新的地址。所以j的内存id发生了变化。

python list,dict可变对象

举栗子

def dict_object():
a
= {}
b
= a
print(id(a))
a[
'a'] = 'hhh'
print('id a:' + str(id(a)))
print('a:' +str(a))
print('id b:' + str(id(b)))
print('b:' + str(b))
if __name__ == '__main__':
dict_object()

 

运行结果

65616352
id a:65616352
a:{'a': 'hhh'}
id b:65616352
b:{'a': 'hhh'}

可以看到a最早的内存地址id是65616352 然后把a赋值给b其实就是让变量b的也指向a所指向的内存
空间。然后我们发现当a发生变化后,b也跟着发生变化了,因为list是可变类型,所以并不会复制
一份再改变,而是直接在a所指向的内存空间修改数据,而b也是指向该内存空间的,自然b也就跟着
改变了。


python函数的参数传递

由于python规定参数传递都是传递引用,也就是传递给函数的是原变量实际所指向的内存空间,
修改的时候就会根据该引用的指向去修改该内存中的内容,所以按道理说我们在函数内改变了传递
过来的参数的值的话,原来外部的变量也应该受到影响。但是上面我们说到了python中有可变类型和
不可变类型,这样的话,当传过来的是可变类型(list,dict)时,我们在函数内部修改就会影响函数
外部的变量。而传入的是不可变类型时在函数内部修改改变量并不会影响函数外部的变量,
因为修改的时候会先复制一份再修改。


举栗子:

def fun1(a_int, b_list):
a_int
= a_int + 1
b_list.append(
'6')
print('inner a_int:' + str(a_int))
print('inner b_list:' + str(b_list))

if __name__ == '__main__':
a_int
= 8
b_list
= [10, 11]

fun1(a_int, b_list)

print('outer a_int:' + str(a_int))
print('outer b_list:' + str(b_list))

 

输出结果

inner a_int:9
inner b_list:[10, 11, '6']
outer a_int:8
outer b_list:[10, 11, '6']

经过fun1()方法修改后,传递过来的int类型外部变量没有发生改变,而list这种可变类型则因为
fun1()方法的影响导致内容发生了改变。

总结:
python只允许引用传递是为方便内存管理,因为python使用的内存回收机制是计数器回收,
就是每块内存上有一个计数器,表示当前有多少个对象指向该内存。每当一个变量不再使用时,
就让该计数器-1,有新对象指向该内存时就让计数器+1,当计时器为0时,就可以收回这块内存了。
当然它肯定不止用了计数器吧,应该还有其他的技术,比如分代回收什么的等


理解可变与不可变对象之后,现在说浅拷贝与深拷贝

浅拷贝仅仅复制了容器中元素的地址
举栗子:

a = ['hello',[1,2,3]]
b
= a[:]
[id(x)
for x in a]
[
62493024, 62128416]
[id(x)
for x in b]
[
62493024, 61778560]
a[0]
='world'
a[
1].append(4)
print(a)
[
'world', [1, 2, 3, 4]]
print(b)
[
'hello', [1, 2, 3, 4]]

 

a和b中元素的地址都是相同的,不可变的hello
和可变的list地址都一样,说明浅拷贝只是将容器内的元素的地址复制了一份。这可以通过修改后,
b中字符串没改变,但是list元素随着a相应改变得到验证。

浅拷贝是在另一块地址中创建一个新的变量或容器,但是容器内的元素的地址均是源对象的元素的
地址的拷贝。也就是说新的容器中指向了旧的元素。

(二)
深拷贝,完全拷贝了一个副本,容器内部元素地址都不一样
举栗子:

a = ['hello',[1,2,3]]
b
= deepcopy(a)
[id(x)
for x in a]
out: [
62493024, 61752504]
[id(x)
for x in b]
out: [
62493024, 62131936]
a[0]
= 'Name'
a[
1].append(7)
print(a)
[
'Name', [1, 2, 4, 7]]
print(b)
[
'hello', [1, 2, 4]]

 

深拷贝后,a和b的地址以及a和b中的元素地址均不同,这是完全拷贝的一个副本,修改a后,
发现b没有发生任何改变,因为b是一个完全的副本,元素地址与a均不同,a修改不影响b。
深拷贝是在另一块地址中创建一个新的变量或容器,同时容器内的元素的地址也是新开辟的,
仅仅是值相同而已,是完全的副本