详解 Python 浅拷贝与深拷贝以及引用

一. Python 变量及其存储

1.1 Python 变量存储情况

在高级语言中,变量是对内存及其地址的抽象。对于python 而言,python 的一切变量都是对象,变量的存储,采用了引用语义的方式,存储的只是一个变量的值所在的内存地址,而不是指这个变量的本身:

  • 引用语义:在 python 中,变量保存的是对象(值)的引用,我们称为 引用语义。采用这种方式,变量所需的存储空间大小一致,因为变量只是保存了一个引用。也被称为对象语义指针语义

  • 值语义:有些语言采用的不是这种方式,它们把变量的值直接保存在变量的存储区里,这种方式被我们称为值语义,例如 C 语言,采用这种存储方式,每一个变量在内存中所占的空间就要根据变量实际的大小而定,无法固定下来。

  • 值语义和引用语义的区别:

    • 值语义: 死的、 傻的、 简单的、 具体的、 可复制的
  • 引用语义: 活的、 聪明的、 复杂的、 抽象的、 不可复制的

1.2 Python 数据类型地址存储

Python 的引用语义和 C 语言值语义在内存中的存储情况如下:

变量的每一次初始化,都会开辟了一个新的空间,将新内容的地址赋值给变量。对于下图来说,如果重复的给 str1 赋值,其在内存中的变化如下右图:


str1 在重复的初始化过程中,str1 中存储的元素地址由 ‘Hello world’ 的地址变成了 ‘new Hello world’ 的。

了解了 Python 变量赋值过程后,就应该明白 Python 的赋值过程相当于完全共享资源,一个值的改变会完全被另一个值共享。而实际应用是往往需要备份出一份数据而只对新值进行修改,这时候赋值就变得不是那么明智。引用 Python 浅拷贝与深拷贝概念为这方面提供了解决方案。

二. 深浅拷贝引入

经过上面讨论,我们知道在 Python 中对对象的赋值其实就是对对象的引用。当创建一个对象,把它赋值给另一个变量的时候,Python 并没有拷这个对象,只是拷贝了这个引用对象而已。

考虑这样一个场景:有一个列表 warmtones 包含几种颜色,每个颜色代表元素类的一个实例。而希望创建一个为名为 palette 的列表,使 palette 具有跟 warmtones 一样的颜色列表,并希望达到在改变 palette 的同时不改变 warmtones。

于是,A,B,C 同学发表了自己的看法。

A 同学:

palette = warmtones

B 同学:

palette = list(warmones)

C 同学:

import copy
palette = copy.deepcopy(warmtones) 

**注意:**为了简化对比说明,使用实例中的颜色不再是一个类的实例,而是使用字符串替代。
先考虑 A 同学,现在想给 warmtones 添加 black 颜色:

很不幸,这并不符合实际要求,因为在对 palette 进行添加颜色的时候,warmtones 也发生了改变。实际上 A 同学的做法,可以用以下图说明:

A 同学的做法相当于 将 warmtonespalette 指向了同一块内存地址,所以无论是改变 warmtones 还是 palette 都会对此内存地址数据产生变化。而导致改变 warmtonespalette 任意一方,都会影响到对方。


对于 B 同学, B同学使用的是一种浅拷贝:

这里对 palette 添加 _black 元素,此时看起来并不会对 warmtones产生影响,这就达到要求了吗?其实,不然,分析图中,很容易见,虽然 warmtonespaletteid 已经不同了, 但它们的元素 id 仍然相同。如下图:

现在对 B 同学的做法使用另一个实例,将原来 warmtones 列表字符串元素换成 Color 类的实例对象

如你所见,直接将 palette 列表中的对象改变,不会影响,warmtones ,因为 yellow 是 Color 的一个新实例,palette 的 id[0] 已经被指向新的地址,故不会影响 warmtones。见下图:

但如果直接对 paltte 地址中的数据进行修改,如将 red 实例对象颜色修改为 yellow, 则此时 warmtones 也发生改变。

也就是说只要改变这些地址相同的元素值,warmtonespalette 还是会互相影响,如图:


对于 C 同学的做法中,palettewarmtones 的深拷贝,palette 的引用对象列表也是从 warmtones 中复制过来的。

结合实例:

此时,可以看到 palette 中所有元素地址已经被指向了另一个地方,warmtones对象全部被引用到了 palette

上面所讨论的问题涉及到 python 引用机制和浅复制及深复制

三. Python 浅拷贝与深拷贝

3.1 什么是浅拷贝、深拷贝?

  • 浅拷贝:只是拷贝了对象的最外围本身(即只拷贝一层),而其内部元素也只是拷贝了一个引用而已(即其他内部对象还只是引用),对象中的其他对象并不复制。

  • 深拷贝:外围以及内部元素都进行拷贝对象本身,而不是引用,其他对象的对象一并复制(即递归赋值所有对象)。

3.2 浅拷贝与深拷贝的区别?

3.3 浅拷贝产生的几种情况

  • 使用切片 [:] 操作
  • 使用工厂函数(如 list/dir/set)
    工厂函数看上去像函数,实质上是类,调用时实际上是生成了该类型的一个实例,就像工厂生产货物一样
  • 使用 copy 模块中的 copy() 函数

四.其他相关

4.1 关于 Python 的可变/不可变对象

  • **不可变对象,该对象所指向的内存中的值不能被改变。**当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。

  • **可变对象,该对象所指向的内存中的值可以被改变。**变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。

Python 中,基本数值类型(int和float)、字符串str、元组 tuple 都是不可变类型。而列表 list、字典dict、集合 set 是可变类型

4.2 不可变对象的深浅拷贝

不可变对象类型,没有被拷贝的说法,即便是用深拷贝,查看 id 是一样的,如果对其重新赋值,也只是新创建一个对象,替换掉旧的而已。

一句话就是,不可变类型,不管是深拷贝还是浅拷贝,地址值和拷贝后的值都是一样的。

4.3 对于可变对象深浅拷贝

可变对象拷贝则:

  • = 号赋值:值相等,地址相等
  • copy 浅拷贝:值相等,地址不相等,对象的对象地址相等
  • deepcopy 深拷贝:值相等,地址不相等,对象的对象地址不相等

总结:

  • 赋值只是改变引用,原来对象数据发生变化,被赋值的 变量也会发送变化
  • 浅拷贝只拷贝一层,拷贝后为两个对立对象,但子对象仍为引用,仍然指向原来的子对象
  • 深拷贝将所有对象数据对立,拷贝对象的对象,原对象的改变不会印象其他独立对象(即递归赋值所有对象)。

【相关链接】

猜你喜欢

转载自blog.csdn.net/qq_36148847/article/details/82982581