Python 赋值引用、浅拷贝、深拷贝详细解析

导语

这个知识点在我看来很绕,每次看了别人的讲解也记住了,然后过不了多久又忘了,所以尝试着自己写下来,加深印象。

变量、对象和引用

首先,我们需要知道变量、对象和引用之间的关系:
变量对象引用关系图
如图:

  1. 变量:变量是没有任何类型的,它相当于一个标签,由我们自己取名,存入系统变量表
  2. 对象:对象是计算机分配的一块内存,它拥有一定的大小来存储相应的值
  3. 引用:引用是从变量到对象的指针,它指向对象的那块内存空间

可变对象和不可变对象

Python中有可变对象和不可变对象:

  1. 可变对象:list、set、dict
  2. 不可变对象(里面的值不可变):str、number、tuple
  3. 对可变对象的修改
>>> list1 = [1, 2, 3]
>>> id(list1)
2254106577480
>>> list1[1] = 'x'
>>> list1
[1, 'x', 3]
>>> id(list1)
2254106577480

::可以看出,对可变对象的修改,不会开辟一块新内存,而是直接在原来的内存地址中进行修改,但它的空间区域可能会变。

  1. 对引用不可变对象的变量的修改:
>>> a = 123
>>> id(a)
140706675978704
>>> a = '123'
>>> id(a)
2254108024256

::变量a引用的是一个int类型的不可变对象,当把a重新引用给一个新对象时,新对象会重新开辟一块内存空间,然后把变量a指向这个新对象,而原来的那个对象如果没用被引用,便会等待被垃圾回收。

赋值引用

  1. 引子
>>> num = [1, 3, 5]
>>> num[1] = num
>>> num
[1, [...], 5]

::是不是感觉有点奇怪,我明明把num赋值给num的第二个元素,但是最后的num值却是循环嵌套的。
::所以我们是不是可以说Python没有赋值,只有引用,上面这个操作相当于创建了一个引用自身的结构,所以导致了无限嵌套。

  1. 可变对象的赋值引用
>>> list2 = [1, 2, 3]
>>> list3 = list2
>>> id(list2), id(list3)
(2254107544072, 2254107544072)
>>> list2[1] = 'x'
>>> list2, list3
([1, 'x', 3], [1, 'x', 3])
>>> id(list2), id(list3)
(2254107544072, 2254107544072)

::可以看出,对可变对象的赋值引用,实质是将两个变量指向同一块内存地址,一个改变,另一个也会跟着改变

  1. 不可变对象的赋值引用
>>> tuple1 = (1, 2, 3)
>>> tuple2 = tuple1
>>> id(tuple1), id(tuple2)
(2254107337208, 2254107337208)

::仍是两个变量指向同一块内存地址

  1. 包含嵌套元素的不可变对象的赋值引用
>>> tuple1 = (1,[2, 3, 4], 5)
>>> tuple2 = tuple1
>>> id(tuple1), id(tuple2)
(2254107488120, 2254107488120)
>>> id(tuple1[1]), id(tuple2[1])
(2254106578056, 2254106578056)
>>> tuple1[1][1] = 'x'
>>> tuple1, tuple2
((1, [2, 'x', 4], 5), (1, [2, 'x', 4], 5))
>>> id(tuple1), id(tuple2)
(2254107488120, 2254107488120)
>>> tuple1[1].append('y')
>>> tuple1, tuple2
((1, [2, 'x', 4, 'y'], 5), (1, [2, 'x', 4, 'y'], 5))

::可以看出,两个变量仍是指向同一块内存,里面任何嵌套元素的修改都会各自影响。

  1. 总结
    赋值引用就是把一个变量所引用的内存地址,再交给另一个变量所指,既两个变量(也可能赋值引用多个)都指向同一块内存地址,故无论修改哪个变量,另外变量都会跟着改变。既“旧瓶装旧酒

浅拷贝

  1. 实质:复制一份引用,新的对象和原来的对象的引用被区分开,但是内部元素的地址引用还是相同的
  2. 方法
    1)列表、元组、字符串的切片操作
    2)对象的拷贝方法:list.copy()
    3)copy模块的copy.copy()方法
  3. 实例
    1)可变对象的浅拷贝
>>> list1 = [1, 2, 3]
>>> list2 = copy.copy(list1)
>>> id(list1), id(list2)
(2254108052808, 2254106577480)
>>> [id(x) for x in list1]
[140706675974800, 140706675974832, 140706675974864]
>>> [id(y) for y in list2]
[140706675974800, 140706675974832, 140706675974864]
>>> list1[1] = 'x'
>>> list1, list2
([1, 'x', 3], [1, 2, 3])
>>> id(list1[1]), id(list2[1])
(2254106006000, 140706675974832)
>>> list1.append('y')
>>> list1, list2
([1, 'x', 3, 'y'], [1, 2, 3])

::经过浅拷贝后,list2中的元素复制了list1中元素的地址引用,但list2本身却重新开辟了一块内存。也就是说,list2和list1这两个容器的内存地址不同,但他们里面的元素的内存地址却是一样的。 我认为这其实是两个不同的列表,因为他们的容器地址不同,尽管里面元素地址一样。
::因此,对于list1中元素的修改:>>> list1[1] = ‘x’ ,并且因为list1[1]原本是不可变对象,故而修改它会重新申请一块内存,再将list1[1]指向这块内存地址,这时list1[1]中的内存地址已经与list2[1]不一样了,因此list2并没有发生改变。
::至于对list1执行append的操作,由于list1与list2容器内存地址不同,故而对list1的append操作不会影响到list2( 注意 :append是对容器的操作,如果要对元素操作需要带上索引

:2)不可变对象的浅拷贝

>>> tuple1 = (1, 2, 3)
>>> tuple2 = copy.copy(tuple1)
>>> id(tuple1), id(tuple2)
(2254107536336, 2254107536336)
>>> [id(x) for x in tuple1]
[140706675974800, 140706675974832, 140706675974864]
>>> [id(y) for y in tuple2]
[140706675974800, 140706675974832, 140706675974864]

::可以看出,不可变对象经过浅拷贝后其内存地址是一样的,当然其内的元素地址也是一样
::其实,对于不可变对象,不存在拷贝,浅拷贝和深拷贝一样,所得到的拷贝对象与原对象的地址是完全一样的,当然也会有特殊情况,那就是含有嵌套可变元素的不可变对象进行深拷贝的情形,后面深拷贝会讲到。

: 3)嵌套可变对象的浅拷贝

>>> list3 = [1, [2, 3, 4], 5]
>>> list4 = copy.copy(list3)
>>> id(list3), id(list4)
(2254108051080, 2254107544072)
>>> [id(x) for x in list3]
[140706675974800, 2254108205576, 140706675974928]
>>> [id(y) for y in list4]
[140706675974800, 2254108205576, 140706675974928]
>>> id(list3[1][1])
140706675974864
>>> id(list4[1][1])
140706675974864
>>> list3[1].append('x')
>>> list3, list4
([1, [2, 3, 4, 'x'], 5], [1, [2, 3, 4, 'x'], 5])
>>> list3[1][0] = 'y'
>>> list3, list4
([1, ['y', 3, 4, 'x'], 5], [1, ['y', 3, 4, 'x'], 5])

::这里可以看出,嵌套的元素([2, 3, 4])其内存地址与浅拷贝后的元素仍然是一样的,且嵌套元素里面的元素内存地址与浅拷贝后的也是一样,故而这个嵌套元素([2, 3 , 4])和拷贝后的嵌套元素是同一个列表,因此,将嵌套元素的第一个元素改为’y’,那么浅拷贝后的元素也会跟着改变。
注意:这与第1)点里面例子不同,若是按第1)点例子的方法理解:嵌套元素里面的是一个不可变元素,对它的修改会重新开辟内存,那么浅拷贝后的嵌套元素里的第一个元素便不会改变,与结果矛盾。然而比较他们的不同点可以发现,第一个例子的两个列表的容器内存地址不同,而这里的两个嵌套列表([2,3,4])容器内存地址一样,内部元素地址一样,故而是同一个列表,因此会发生改变)

:4)嵌套不可变对象的浅拷贝

>>> eg = [1, (2, 3, 4), 5]
>>> eg2 = copy.copy(eg)
>>> id(eg), id(eg2)
(2254108205320, 2254108205448)
>>> [id(x) for x in eg]
[140706675974800, 2254107337208, 140706675974928]
>>> [id(y) for y in eg2]
[140706675974800, 2254107337208, 140706675974928]

::这里与第3)点差不多,嵌套元素的地址仍是一样,但不可变对象里的内容不可改变,这里就不多说了。

  1. 总结
    1)对于可变对象来说,浅拷贝相当于“新瓶装旧酒”,外层容器地址改变,内部元素地址不变。
    2)对于不可变对象,被浅拷贝后的对象与原对象仍然内存地址一样。

深拷贝

  1. 实质:拷贝原对象中的元素,不仅新的对象和原来的对象的引用被区分开,而且内部所有嵌套的元素的引用都会被区分开。也就是递归拷贝,它会把所有嵌套的元素都拷贝一下,然后独立引用出来。于是,拷贝与被拷贝对象互不干扰。
  2. 方法:copy模块的deepcopy()方法
  3. 实例
    1)可变对象的深拷贝
>>> list1 = [1, 2, 3]
>>> listt2 = copy.deepcopy(list1)
>>> list1, list2
([1, 2, 3], [1, 2, 3])
>>> id(list1), id(list2)
(2254108205640, 2254106577480)
>>> [id(x) for x in list1]
[140706675974800, 140706675974832, 140706675974864]
>>> [id(y) for y in list2]
[140706675974800, 140706675974832, 140706675974864]
>>> list1[0] = 'x'
>>> list1, list2
(['x', 2, 3], [1, 2, 3])
>>> id(list1[0]), id(list2[0])
(2254106006000, 140706675974800)

::可以看出,可变对象的深拷贝会使容器地址改变,但是为什么元素地址似乎也没变呢?因为,里面的元素是不可变类型,若要修改引用不可变类型的变量,只得重新开辟一块内存,然后指向这块内存地址,这样便与原来的元素区分开了,于是一个对象发生变化,另一个不会随之变化。

:2)嵌套可变对象的深拷贝

>>> list3 = [1, [2, 3, 4], 5]
>>> list4 = copy.deepcopy(list3)
>>>> id(list3), id(list4)
(2254106578056, 2254108051080)
>>> [id(x) for x in list3]
[140706675974800, 2254108205512, 140706675974928]
>>> [id(y) for y in list4]
[140706675974800, 2254108205896, 140706675974928]
>>> id(list3[1][1]), id(list4[1][1])
(140706675974864, 140706675974864)
>>> list3[1][1] = 'x'
>>> list3, list4
([1, [2, 'x', 4], 5], [1, [2, 3, 4], 5])
>>> list3[1].append('y')
>>> list3, list4
([1, [2, 'x', 4, 'y'], 5], [1, [2, 3, 4], 5])

::果然,深拷贝后的两个对象里的不可变对象内存地址仍然不会变化,但是里面嵌套的可变元素的内存地址却不一样,为什么呢?因为可变元素修改不会开辟新的内存,而是在原来内存上修改,于是为了实现深拷贝的目的,那么在深拷贝后只有为嵌套的可变元素重新开辟一块内存空间,这样,两个对象的变化就不会对彼此造成影响。

:3)不可变对象的深拷贝

>>> tuple1 = (1, 2, 3)
>>> tuple2 = copy.deepcopy(tuple1)
>>> id(tuple1), id(tuple2)
(2254107338288, 2254107338288)

::前面已经说到过,不可变对象的深浅拷贝都一样,拷贝后对象的内存地址都不会变。
::但是有下面的一种特殊情况:那就是嵌套了可变元素的不可变对象的深拷贝。

:4)嵌套了可变对象的不可变对象的深拷贝

>>> tuple3 = (1,[2, 3, 4], 5)
>>> tuple4 = copy.deepcopy(tuple3)
>>> id(tuple3), id(tuple4)
(2254107488120, 2254108191408)
>>> [id(x) for x in tuple3]
[140706675974800, 2254107544072, 140706675974928]
>>> [id(y) for y in tuple4]
[140706675974800, 2254108205192, 140706675974928]
>>> id(tuple3[1][1]), id(tuple4[1][1])
(140706675974864, 140706675974864)
>>> tuple3[1].append('x')
>>> tuple3, tuple4
((1, [2, 3, 4, 'x'], 5), (1, [2, 3, 4], 5))
>>> tuple3[1][0] = 'y'
>>> tuple3, tuple4
((1, ['y', 3, 4, 'x'], 5), (1, [2, 3, 4], 5))

::可以看出,嵌套了可变元素的不可变对象经过深拷贝后其容器地址仍然会改变,其内部元素的不可变元素内存地址不变,可变元素内存地址变化,满足上面我们分析的情况。

  1. 总结:
    1)可变对象:可变对象深拷贝后,容器地址改变,内部可变元素地址改变,不可变元素地址不变。相当于“新瓶装新酒”。
    2)不可变对象:没有嵌套可变元素时的深拷贝得到的对象内存地址与原对象相同,当嵌套有可变元素时,容器地址改变,内部不可变元素地址不变,嵌套的可变元素地址改变。

写在最后

自己自学整理,若有错误,还请指正!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值