荐 python之赋值、浅拷贝和深拷贝
概念
python中的对象包含三个属性,id、type和value,id代表着对象唯一的标识符,是独一无二的,cpython中代表了存放对象的内存地址;type代表着对象的类型,比如说数字1的type就是int,字符串‘abc’的type就是str,这里还可以进一步去区分type()函数与isinstance()函数的区别,简单来说type函数不考虑继承,不会认为子类的对象属于父类,而isinstance函数考虑继承;value就是代表我们赋给对象的值。
深拷贝和浅拷贝来自于python的copy模块,可以通过import copy来导入该模块。
浅拷贝的语法为copy.copy(),深拷贝的语法为copy.deepcopy()。
变量及存储方式
值语义与引用语义
在高级语言中,变量的存储方式有两个,值语义和引用语义。
- 对于值语义,大部分语言中都是一样的,把变量的值直接存放在变量的存储区里,这样一来,每个变量所需存储区的大小是不一样的,存放数字和存放字符串所需空间大小就不同,这就需要在给变量赋值时根据值的类型声明变量的类型,静态语言比如c,c++都有值语义。
- 对于引用语义,变量中存储的只是值的引用,比如我们将整数1赋值给变量a,a中存储的是其值的引用(即地址),值1有另外的存储位置,这样做的好处是我们在给变量赋值时不需要提前声明,因为变量存储的是值对象的引用,那所有变量在内存中的大小都是一样的,就是一个地址。也被称为对象语义和指针语义。
python对象的存储方式
在python中,万物皆对象,采用的就是引用语义,赋给变量的值作为对象拥有自己的存储空间,赋值给变量,变量中存储的只是值对象的内存地址,而不是变量本身。
python中的对象可以简单的分为可变对象和不可变对象,基本数据类型比如int,float,bool是不可变的(python2中还有long),结构数据类型中,tuple,str是不可变对象,list,dict,set是可变对象,之所以是可变的,是因为变量中存储的是值对象的地址,值的地址里面存放的是元素值的地址,可以一直这样链式传递下去,所以我们对其进行内置操作(比如append,pop,remove等)都不会改变变量中存储的地址,也就表现为对象是可变的,比如说,a=[1, 2, 3],a存储的是对象[1, 2, 3]的地址,[1, 2, 3]中存储的是元素1,2,3的地址,因此要注意的是仅限于python的内置操作,别的操作比如赋值操作会改变变量的引用,即使与原来的数据一样。
a = [1, 2, 3]
print(id(a))
a = [1, 2, 3]
print(id(a))
a.append(4)
print(id(a), a)
a.pop(0)
print(id(a), a)
b = [7, 8, 9]
print(id(b))
输出结果:
6054472
6054536
6054536 [1, 2, 3, 4]
6054536 [2, 3, 4]
6054472
上述结果体现了python内置操作不会改变对象的引用,赋值操作会改变对象的引用,但是如果是c = a,那就没问题了,因为a本来就是存储着值对象的地址,赋值操作后,传递给c的实际上也是值对象的地址,所以id并不会变,有意思的是最后的b的id竟然与第一个a的id一样,这说明了当变量重新指向新的内存地址后,之前指向的内存就会被回收,建立新的变量时,又从程序自己的内存空间空间开始位置查找可分配的内存。
下面再看一个有意思的操作:
a = [1, 2, 3]
print(id(a))
a[0] = 4
print(id(a))
a[2] = [1, 2, 3]
print(id(a))
输出结果:
5071432
5071432
5071432
对列表中的元素进行赋值操作,并没有改变变量中存储的地址,但是按照前面的说法,赋值操作会改变变量指向的地址,这不是前后矛盾吗?
实际上并不矛盾,个人的理解是这样的:
前面我们也提到了,对于list这种复杂的数据结构,变量只是值对象的引用,存储值对象的地址,值对象中存储着各个元素的地址,因此,我们对元素进行赋值操作只是修改值对象中存储的单个的元素地址,并不涉及变量对值对象引用这一层面,所以变的只是元素的地址,变量的id不会变。
a = [1, 2, 3]
print(id(a), id(a[0]), id(a[1]))
a[0] = 4
print(id(a), id(a[0]), id(a[1]))
a[1] = [3, 5, 6]
print(id(a), id(a[0]), id(a[1]))
输出结果:
30237256 8791455224864 8791455224896
30237256 8791455224960 8791455224896
30237256 8791455224960 30237320
对列表a中的元素进行赋值操作,会发现对象a的id没变,但被改变的元素的id变了。
明白了对象的存储方式,赋值,浅拷贝和深拷贝的区别就显而易见了
赋值操作
不可变对象
对于不可变对象,每次赋值都会对变量重新初始化,改变其指向的内存地址。改变其中的一个,并不会改变另一个。
a = 1
b = a
print(a, b, id(a), id(b), id(1))
b = 2
print(a, b, id(a), id(b))
输出结果:
1 1 8791455224864 8791455224864 8791455224864
1 2 8791455224864 8791455224896
对于第一次输出中,id(a),id(b),id(1)相等的情况,个人理解与python的内存管理有关,对于不可变数据类型,变量就是值对象地址的另一种表述方式,不会为这个变量名字单独开辟地址空间存储,因为对于不可变对象来说,只要改变值,地址一定会变,因此没必要专门为变量名开辟地址空间。
可变对象
相比于不可变对象,变量与各元素值对象之间多了一层对各元素值对象的引用,这时变量名会有自己的存储空间,与值对象的id不同。
c = [7, 8, 9]
print(id(c), id([7, 8, 9]))
c[1] = [123, 456]
print(id(c))
输出结果:
39478024 31499656
39478024
a = [1, 2, 3]
b = a
print(id(a), id(b), id([1, 2, 3]))
b[0] = 4
print(a, b, id(a), id(b))
b = [4, 5, 6]
print(a, b, id(a), id(b))
输出结果:
30630472 30630472 30630536
[4, 2, 3] [4, 2, 3] 30630472 30630472
[4, 2, 3] [4, 5, 6] 30630472 30630536
赋值操作后,只改变b内部变量的值,a与b同时改变,a与b的id不变,如果直接将值对象赋给b,此时b就成为了一个全新的对象。
浅拷贝
注意:只有可变对象才有深浅拷贝之分,不可变对象都是一样的
首先要明白为什么有了赋值操作还要拷贝操作?
在程序中,我们经常要对数据进行修改,比如有一个数组list1 = [1, 2, …],要修改list1中的数据,如果原数据在程序后面用不到了,可知直接在list1中修改,但如果原数据还有用,就不能直接在原数据中修改,要另外生成一个数组元素相同的新的对象,如果直接用list2 = list1生成,修改list2,list1也会改变,与我们的初衷不符,直接复制操作的话,list2 = [1, 2, …],数据少的话还可以,数据很多会非常麻烦,这时候,引入拷贝操作,list2 = copy.copy(list1),非常方便而且安全。
浅拷贝:直将第一层的元素进行拷贝,重新生成地址引用,原对象和拷贝对象是两个不同的对象,但是第二层的元素的地址引用还是相同的,因此修改第一层元素只有被修改的变量会变,但修改第二层元素,两个变量的值都会变。
```python
import copy
a = [1, 2, 3, [4, 5, 6]]
b = copy.copy(a)
print(a, id(a))
print(b, id(b), end='\n\n')
b[1] = 7
print(a)
print(b, end='\n\n')
b[3].append(8)
print(a, id(a))
print(b, id(b))
输出结果:
[1, 2, 3, [4, 5, 6]] 39157448
[1, 2, 3, [4, 5, 6]] 39215368
[1, 2, 3, [4, 5, 6]]
[1, 7, 3, [4, 5, 6]]
[1, 2, 3, [4, 5, 6, 8]] 39157448
[1, 7, 3, [4, 5, 6, 8]] 39215368
深拷贝
既然浅拷贝是只拷贝一层,那么深拷贝当然就是完全的拷贝,不管对象里面嵌套了基层数据结构,直接拷贝到元素为不可变对象时才结束,这样拷贝出来的新对象就和原对象完全没有关系,无论如何修改,都不会对另一个起到影响。
import copy
a = [1, 2, 3, [4, 5, [6, 7, 8]]]
b = copy.deepcopy(a)
print(a, id(a))
print(b, id(b), end='\n\n')
b[1] = 7
print(a)
print(b, end='\n\n')
b[3].append(9)
print(a)
print(b, end='\n\n')
b[3][2].append(10)
print(a, id(a))
print(b, id(b))
输出结果:
[1, 2, 3, [4, 5, [6, 7, 8]]] 39346440
[1, 2, 3, [4, 5, [6, 7, 8]]] 39346888
[1, 2, 3, [4, 5, [6, 7, 8]]]
[1, 7, 3, [4, 5, [6, 7, 8]]]
[1, 2, 3, [4, 5, [6, 7, 8]]]
[1, 7, 3, [4, 5, [6, 7, 8], 9]]
[1, 2, 3, [4, 5, [6, 7, 8]]] 39346440
[1, 7, 3, [4, 5, [6, 7, 8, 10], 9]] 39346888
拓展:切片操作
python中对于list有一个魔术方法,简单实用且功能强大,就是切片操作,切片操作可以从目标对象中切取部分或全部的值,那么问题来了,切片操作后的数组与原数组是什么关系,切片操作属于赋值?浅拷贝?深拷贝?
a = [1, 2, [3, 4, 5]]
b = a[:]
print(a, id(a))
print(b, id(b), end='\n\n')
a[1] = 6
print(a, id(a))
print(b, id(b), end='\n\n')
a[2].append(7)
print(a, id(a))
print(b, id(b), end='\n\n')
输出结果:
[1, 2, [3, 4, 5]] 30433928
[1, 2, [3, 4, 5]] 39216072
[1, 6, [3, 4, 5]] 30433928
[1, 2, [3, 4, 5]] 39216072
[1, 6, [3, 4, 5, 7]] 30433928
[1, 2, [3, 4, 5, 7]] 39216072
根据输出结果来看,切片操作后,两个对象的id不同,说明切片操作不是赋值,当改变其中一个的第一层元素时,另一个的元素值不变,但是当改变第二层元素时,两个对象的第二层元素都改变了,至此,我们知道了,切片操作是浅拷贝。
本片文章就分享到这了,有任何问题可以在下方评论。
更多关于内容可以移步我的我的博客,热爱生活,分享知识。
本文地址:https://blog.csdn.net/qq_43068854/article/details/107291081