欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 python之赋值、浅拷贝和深拷贝

程序员文章站 2022-06-22 16:55:10
概念python中的对象包含三个属性,id、type和value,id代表着对象唯一的标识符,是独一无二的,cpython中代表了存放对象的内存地址;type代表着对象的类型,比如说数字1的type就是int,字符串‘abc’的type就是str,这里还可以进一步去区分type()函数与isinstance()函数的区别,简单来说type函数不考虑继承,不会认为子类的对象属于父类,而isinstance函数考虑继承;value就是代表我们赋给对象的值。深拷贝和浅拷贝来自于python的copy模块,...

概念

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