Python基础-17对象引用和拷贝
17.对象引用和拷贝
我们先来看看以下向个概念
- 变量:是系统变量名表中的元素,通常是由程序员进行定义声明
- 对象:是计算机分配的一块内存,需要足够的空间去表示它的值
- 引用:是自动形成的从变量到对象的指针
- 可变对象:允许对自身内容进行修改。如list、dict、set、自定义类型等。
- 不可变对象:不允许对自身内容进行修改。如果对一个不可变对象进行赋值,实际上是生成一个新的对象,再让变量指向这个对象。如int、float、bool、str、tuple
如果有了解Java的堆栈知识(堆存储真实的数据,而栈则是存储相应引用地址),则这里所指的对象可以理解为堆,而引用则代表栈。
17.1 对象引用
对象的赋值实际上就是对象引用,创建一个对象并将其赋值给一个变量时,该变量实际是指向了该对象的引用,可使用内置函数id()查看返回值。变量名与对象之间的示意图如下所示:
170101对象与变量名关系.png示例如下所示:
>>> tempA=[1,3,5]
>>> tempB=tempA # tempB对tempA的引用
>>> tempB
[1, 3, 5]
>>> tempB[0]=-100 # 修改tempB的元素,tempA相应的元素也同步进行了更改
>>> tempA
[-100, 3, 5]
>>> tempB
[-100, 3, 5]
>>> id(tempA),id(tempB)
(2614814009544, 2614814009544)
>>> tempB is tempA
True
在上面的例子中,本意是想修改tempB中第一个元素,而连带temA也被一起修改了。因为tempA和tempB引用的是同一个对象,修改其中任意一个变量都会影响到另一个。为了避免这种情况,必须创建对象的副本而不是创建新引用。对于像列表和字典这种容器类对象,可以使用两种拷贝操作:浅拷贝和深拷贝。
17.2 对象的拷贝
17.2.1 浅拷贝
浅拷贝将创建一个新对象,其内容是原对象中元素的引用。可以使用模块copy中的copy()函数,另外也可使用切片操作、对象的copy方法。其特点如下所示:
- 两个变量的内存地址不同
- 变量之间存在共享值的情况
- 对其中一个变量进行更改后,另外的变量也会随之改变
如果使用等号赋值时,连对象都不会重新创建。只有重新创建对象并为其赋值,才会发生浅拷贝。
示例代码如下所示:
>>> a=[1,2,[3,4]]
>>> b=list(a) # 创建a的一个浅复制
>>> b is a
False
>>> b.append(100) # 给b追加一个元素
>>> b
[1, 2, [3, 4], 100] # 修改b中的一个元素
>>> a
[1, 2, [3, 4]]
>>> b[2][0]=-98
>>> b
[1, 2, [-98, 4], 100]
>>> a # a中与b共有的元素值也会发生改变
[1, 2, [-98, 4]]
>>> id(a),id(b)
(2614813897288, 2614813796232)
>>> aa=[1,2,[3,4]]
>>> bb=aa # 直接赋值并没有发生浅拷贝
>>> id(aa),id(bb)
(1960262980168, 1960262980168)
>>> aa = list(bb)
>>> id(aa),id(bb)
(1960263019208, 1960262980168) # 发生了浅拷贝,因此两者的id也不一样
>>> id(aa[0]),id(aa[1]),id(aa[2])
(140715523797264, 140715523797296, 1960263020232)
>>> id(bb[0]),id(bb[1]),id(bb[2])
(140715523797264, 140715523797296, 1960263020232) # 虽然发生了浅拷贝,但内部元素却都指向相同的对象
在上述示例中,a和b是单独的列表对象,但它们包含的元素是共享的。因此修改b的一个元素也会修改a中的对应元素。而在aa和bb中,在发生浅拷贝后,aa和bb两个对象的地址不一样,而其内部元素却指向了相同的对象。
17.2.2 深拷贝
深拷贝将创建一个新对象并对其赋值时,原对象中的所有元素都会在新对象中重新创建一次。常用模块copy中的deepcopy()函数,其特点如下所示:
- 变量间的内存地址不同
- 变量间有各自的值,且互不影响
- 对其任意一个变量的值进行修改,不会影响另外一个
示例代码如下所示:
>>> import copy
>>> a=[1,2,[3,4]]
>>> b=copy.deepcopy(a) # 深拷贝
>>> b is a
False
>>> b.append(100)
>>> b
[1, 2, [3, 4], 100]
>>> a
[1, 2, [3, 4]]
>>> b[2][0]=-98
>>> b
[1, 2, [-98, 4], 100]
>>> a # 在修改b之后,对a没有任何影响
[1, 2, [3, 4]]
>>> id(a),id(b)
(1960263017096, 1960263112136)
>>> aa=[1,2,[3,4]]
>>> bb=copy.deepcopy(aa) # 深拷贝
>>> id(aa),id(bb)
(2540655565384, 2540656480520) # 地址发生改变
>>> id(aa[0]),id(aa[1]),id(aa[2])
(140715523797264, 140715523797296, 2540655563912)
>>> id(bb[0]),id(bb[1]),id(bb[2])
(140715523797264, 140715523797296, 2540656401224)
在打印内部地址发现,前两个元素地址没有属性改变,是因为在Python数字和字符串属于不可变对象。为提升效率,Python语言中,在内存中只存在一份不可变对象,并将其地址(即引用)赋值给其他变量。
浅拷贝和深拷贝仅仅是针对可变对象的,对于不可变对象,赋值的操作过程都是直接将引用赋值。
17.3 小结
现在假设有一个对象a=[ 1, 2 ,[ 3,4 ] ],有另外一个对象,分别进行=赋值、浅拷贝和深拷贝,其使用小结如下所示:
- 1.使用=直接赋值,不会发生浅拷贝和深拷贝情况,仅相当于增加一个新标签,并不产生新的对象,示意图如下所示:
针对这种情况,有时候也被比喻为旧瓶装旧酒。
- 2.使用浅拷贝之后,会创建一个新的对象,但内部元素仍然保持一致,示意图如下所示:
因为元素中1和2为不可变对象,它们互不影响,给人的感觉就相当于复制了一份。这种就是浅拷贝,有时候也被比喻为新瓶装旧酒,虽然产生了新的对象,但里面的内容还是来自同一份。
- 3.使用深拷贝之后,会创建一个新的对象,原对象中的所有元素会被重新创建一次,示意图如下所示:
对象a和b前两个元素因是不可变对象,所会在进行深拷贝之后,地址不会进行更改。而第三个元素为可变对象,则相当创建了一个副本。所以深拷贝也可以理解为,不仅是对象自身的拷贝,对于对象中每一个子元素,也都进行同样的拷贝。针对这种情况,有时候也被比喻为新瓶装新酒
- 4.浅拷贝和深拷贝针对的是可变对象