详解Python核心编程中的浅拷贝与深拷贝
作者:laozhang 发布时间:2021-05-04 15:44:09
一、问题引出浅拷贝
首先看下面代码的执行情况:
a = [1, 2, 3]print('a = %s' % a) # a = [1, 2, 3]b = aprint('b = %s' % b) # b = [1, 2, 3]a.append(4) # 对a进行修改print('a = %s' % a) # a = [1, 2, 3, 4]print('b = %s' % b) # b = [1, 2, 3, 4]b.append(5) # 对b进行修改print('a = %s' % a) # a = [1, 2, 3, 4, 5]print('b = %s' % b) # b = [1, 2, 3, 4, 5]
上面的代码比较简单,定义了一个变量a,它是一个数值[1, 2, 3]的列表,通过一个简单的赋值语句 b = a 定义变量b,它同样也是数值[1, 2, 3]的列表。
问题是:如果此时修改变量a,对b会有影响吗?同样如果修改变量b,对a又会有影响吗?
从代码运行结果可以看出,无论是修改b还是修改a(注意这种修改的方式,是用append,直接修改原列表,而不是重新赋值),都另一方都是有影响的。
当然这个原因其实很好理解,变量a指向的是列表[1, 2, 3]的地址值,当用 = 进行赋值运算时,b的值也相应的指向的列表[1, 2, 3]的地址值。在python中,可以通过id(变量)的方法来查看地址值,我们来查看下a,b变量的地址值,看是不是相等:
# 注意,不同机器上,这个值不同,但只要a,b两个变量的地址值是一样的就能说明问题了print(id(a)) # 4439402312print(id(b)) # 4439402312
所以原理如下图所示:
因此,只要是在地址值:4439402312上的列表进行修改的话,a,b都会发生变化。(注意我这里说的修改,是在地址值为:4439402312上的列表进行的修改,而不说对变量a进行修改,因为对变量a的修改方式有两种,本文结尾会解释为什么不说对变量a进行修改) 。所以我们便引出了以下概念:
对于这种是将引用进行拷贝赋值给另一个变量的方式(即拷贝的是地址值),我们称之为浅拷贝。
二、如何进行深拷贝
python中实现深拷贝的方式很简单,只需要引入copy模块,调用里面的deepcopy()的方法即可,示例代码如下:
import copya = [1, 2, 3]b = copy.deepcopy(a)print('a = %s' % a) # a = [1, 2, 3]print('b = %s' % b) # b = [1, 2, 3]b.append(4)print('a = %s' % a) # a = [1, 2, 3]print('b = %s' % b) # b = [1, 2, 3, 4]
从代码执行情况来看,我们已经实现了深拷贝。这时我们再来看下两个变量的地址值:
print(id(a)) # 4321416008print(id(b)) # 4321416200
果然就不一样了。我们再通过一个图来看下深拷贝的原理:
三、copy模块方法简介
从深拷贝的实现过程,我们知道copy模块,也使用了里面的deepcopy()方法。下面我们来介绍下copy模块中的copy()与deepcopy()方法。
首先介绍我们已经使用过的deepcopy()方法,官方文档介绍如下:
简单解释下文档中对这个方法的说明:
1. 返回值是对这个对象的深拷贝
2. 如果拷贝发生错误,会报copy.err异常
3. 存在两个问题,第一是如果出递归对象,会递归的进行拷贝,第二正因为会递归拷贝,会导致出现拷贝过多的情况
4. 关于两种拷贝方式的区别都是相对是引用对象
前两点很好理解,针对第三点,我们用代码进行解释:
import copya = [1, 2, 3]b = [3, 4, 5]c = [a, b] # 列表嵌套d = copy.deepcopy(c)print('c = %s' % c) # c = [[1, 2, 3], [3, 4, 5]]print('d = %s' % d) # d = [[1, 2, 3], [3, 4, 5]]c.append(4)print('c = %s' % c) # c = [[1, 2, 3], [3, 4, 5], 4]print('d = %s' % d) # d = [[1, 2, 3], [3, 4, 5]]c[0].append(4) # 相当于a.append(4)print('c = %s' % c) # c = [[1, 2, 3, 4], [3, 4, 5], 4]print('d = %s' % d) # d = [[1, 2, 3], [3, 4, 5]]# a.append(4)# print('c = %s' % c) # a = [1, 2, 3]# print('d = %s' % d) # b = [1, 2, 3]print(id(c)) # 4314188040print(id(d)) # 4314187976print(id(c[0])) # 4314186568print(id(d[0])) # 4314187912print(id(a)) # 4314186568print(id(b)) # 4314186760
根据代码,我们可以看到,当有嵌套对象,也就是文档中提到的递归对象,从结果我们可以看到,嵌套对象会进行递归的深拷贝。即如果c里有一个a,那么不仅c会深拷贝,a同样也会被深拷贝。原理如下图所求:
接下来我们再来看copy()方法:
官方文档解释的很简单,它返回的就是对象的浅拷贝。但其实它会对最外层进行深拷贝,而如果有多层,第二层以后进行的就是浅拷贝了。代码示例如下:
import copya = [1, 2, 3]b = [3, 4, 5]c = [a, b] # 列表嵌套d = copy.copy(c)print('c = %s' % c) # c = [[1, 2, 3], [3, 4, 5]]print('d = %s' % d) # d = [[1, 2, 3], [3, 4, 5]]c.append(4)print('c = %s' % c) # c = [[1, 2, 3], [3, 4, 5], 4]print('d = %s' % d) # d = [[1, 2, 3], [3, 4, 5]] 没有发生变化,说明外层是深拷贝c[0].append(4) # 相当于a.append(4)print('c = %s' % c) # c = [[1, 2, 3, 4], [3, 4, 5], 4]print('d = %s' % d) # d = [[1, 2, 3, 4], [3, 4, 5]] 发生了变化,说明内层是浅拷贝# a.append(4)# print('c = %s' % c) # c = [[1, 2, 3, 4], [3, 4, 5], 4]# print('d = %s' % d) # d = [[1, 2, 3, 4], [3, 4, 5]] 发生了变化,说明内层是浅拷贝print(id(c)) # 4322576648print(id(d)) # 4322576584 d和c地址不同,进一步说明外层是深拷贝print(id(c[0])) # 4322575176print(id(d[0])) # 4322575176 c[0]和d[0]地址相同,进一步说明内层是浅拷贝print(id(a)) # 4322575176print(id(b)) # 4322575368
【注意】对于copy()方法,有特殊情况,比如元组类型,代码示例如下:
import copya = [1, 2, 3]b = [3, 4, 5]c = (a, b) # 列表改成元组d = copy.copy(c)print(id(c)) # 4303015752print(id(d)) # 4303015752 d和c地址相同print(id(c[0])) # 4322575176print(id(d[0])) # 4322575176 c[0]和d[0]地址相同,进一步说明内层是浅拷贝
可以看到,这里哪怕是最外层,也是浅拷贝。
这里因为copy方法内部有判断,如果最外层的拷贝类型是不可变类型,则进行浅拷贝,反之则进行深拷贝。
至此,我们应该对浅拷贝的概念进行进一步加深理解:
如果对象中的所有元素,有一个是引用拷贝,则定义为是浅拷贝。(该定义不是官方定义,只是个人理解)
四、关于“修改”的一点说明
前面提到了修改变量,我认为修改是有两种方式,第一种在原对象上进行修改,第二种就是重新赋值。看如下代码:
import copya = [1, 2, 3]b = aa = [3, 4, 5]print(a) # [3, 4, 5]print(b) # [1, 2, 3]
同样是浅拷贝,但是发现修改a之后,b没有发生变化。
在修改的时候,我们很容易想当然的通过重新赋值的方式来修改,但其实这种修改方式是有问题的。当给a再次赋值的时候,其实是将a重新指向了另外一块地址区域,而原来的[1, 2, 3]那块地址区域是没有发生任何变化的,所以对于b来说,它指向的东西并没有改变。
这也解释了之前文档中关于deepcopy方法的一个说明,为什么只对引用对象有用,因为简单类型修改的方式就是重新赋值。简单理解就是你没办法通过简单类型的变量直接通过.来调用自身的方法,都只能重新赋值来改变,那么都会指向新的地址。
来源:https://www.cnblogs.com/yrrAwx/p/8202838.html
猜你喜欢
- 使用Python的人都知道range()函数和list很方便,今天再用到他的时候发现了很多以前看到过但是忘记的细节。这里记录一下range(
- 下面一段代码给大家分享php未登录自动跳转到登录页面,具体代码如下所示:<?php namespace Home\Controller
- Template无疑是一个好东西,可以将字符串的格式固定下来,重复利用。同时Template也可以让开发人员可以分别考虑字符串的格式和其内容
- 在odoo中,通过iframe嵌入 html,页面数据则通过controllers获取,使用jinja2模板传值渲染html页面分页内容,这
- 安装paramiko后,看下面例子:import paramiko#设置ssh连接的远程主机地址和端口t=paramiko.Transpor
- 前言:👉对于新手来说,库的安装是遇到的第一个挑战,我也入了很多坑,所以想出一期安装库的步骤,由于博主水平限制,博客难免会有错误和不准之处,我
- 1 前言在前面的章节中我们牛刀小试,一直在使用python爬虫去抓取数据,然后把数据信息存放在数据库中,至此已经完成了基本的基本信息的处理,
- wheel文件Wheel和Egg都是python的打包格式,目的是支持不需要编译或制作的安装过程,实际上也是一种压缩文件,将.whl的后缀改
- 在Microsoft SQL Server 2000中,用于数据存储的实用工具是数据库。数据库的物理表现是操作系统文件,即在物理上,一个数据
- 最近写了个python抓取必应搜索首页http://cn.bing.com/的背景图片并将此图片更换为我的电脑桌面的程序,在正则匹配图片ur
- 本文实例讲述了python中随机函数random用法。分享给大家供大家参考。具体如下:python中的random模块功能非常强大,可以生成
- tbody 标签表格主体(正文)。该标签用于组合 HTML 表格的主体内容。tbody 元素应该与&
- 1、执行cmd指令,在cmd输出的内容会直接在控制台输出,返回结果为0表示执行成功。2、在调用完shell脚本后,返回一个16位的二进制数,
- 场景对分页来说,我们最感兴趣的是下面几个信息总共有多少页当前是第几页是否可以上一页和下一页代码下面代码演示如何获取分页总数及当前页数、跳转到
- 一、CSRF:保护机制Django预防CSRF攻击的方法是在用户提交的表单中加入一个csrftoken的隐含值,这个值和服务器中保存的csr
- 这个项目到一开始的kickoff到现在,持续了很长的一段时间,现在差不多也接近了尾声,所以要好好做个总结,下面不会设计到太多技术层面上的东西
- 概述从今天开始我们将开启一段自然语言处理 (NLP) 的旅程. 自然语言处理可以让来处理, 理解, 以及运用人类的语言, 实现机器语言和人类
- function getElementsByClassName(elem_name,elem_tags) { //elem_name:查询的
- 1、注释单行注释,使用#,#号后面的都是注射,例如#我是单行注释print("Hello Python world")多
- Python 中的 Operator 模块可以让它支持函数式编程。1 计算函数假设我们需要一个计算阶乘的函数,一般做法是使用递归。如果使用函