python中copy和deepcopy区别?

2020-07-30 19:58发布

2条回答
杨晓春
2楼 · 2020-07-31 09:22

深复制被复制对象完全再复制一遍作为独立的新个体单独存在。所以改变原有被复制对象不会对已经复制出来的新对象产生影响。 

浅复制并不会产生一个独立的对象单独存在,他只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。


爱学习的蜗牛
3楼 · 2020-09-02 10:24






同样是copy,二者有什么不同呢今天我们就一探究竟!!!

关于copy()和deepcopy()的第一篇博客


  初学编程的小伙伴都会对于深浅拷贝的用法有些疑问,今天我们就结合python变量存储的特性从内存的角度来谈一谈赋值和深浅拷贝~~~

预备知识一——python的变量及其存储

  在详细的了解python中赋值、copy和deepcopy之前,我们还是要花一点时间来了解一下python内存中变量的存储情况。

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

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

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

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

值语义:死的、傻的、简单的、具体的、可复制的

引用语义:活的、聪明的、复杂的、抽象的、不可复制的


引用语义与值语义详解

  我们来看一张简单易懂的图理解一下python的引用语义和C语言值语义在内存中的存储情况,左右两个图,分别表示了python中变量存储与C语言中变量存储区别:

  

预备知识二——各基本数据类型的地址存储及改变情况

  在python中的数据类型包括:bool、int、long、float、str、set、list、tuple、dict等等。我们可以大致将这些数据类型归类为简单数据类型和复杂的数据结构。

我的划分标准是,如果一个数据类型,可以将其他的数据类型作为自己的元素,我就认为这是一种数据结构。

数据结构的分类有很多种,但是在Python中常用的只有集合、序列和映射三种结构。

对应python中的set、list(tuple、str)、dict;常用的数据类型有int、long、float、bool、str等类型。

(其中,str类型比较特别,因为从C语言的角度来说,str其实是一个char的集合,但是这与本文的关联不大,所以我们暂时不谈这个问题)


数据结构及其划分依据

 

  

  由于python中的变量都是采用的引用语义,数据结构可以包含基础数据类型,导致了在python中数据的存储是下图这种情况,每个变量中都存储了这个变量的地址,而不是值本身;对于复杂的数据结构来说,里面的存储的也只只是每个元素的地址而已。:

  

  1. 数据类型重新初始化对python语义引用的影响

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

  3.         

  4.   从上图我们可以看出,str1在重复的初始化过程中,是因为str1中存储的元素地址由’helloworld’的地址变成了’newhelloworld’的。

  5.   2.数据结构内部元素变化重对python语义引用的影响

  6.   对于复杂的数据类型来说,改变其内部的值对于变量的影响:

  7.        

  8.   当对列表中的元素进行一些增删改的操作的时候,是不会影响到lst1列表本身对于整个列表地址的,只会改变其内部元素的地址引用。可是当我们对于一个列表重新初始化(赋值)的时候,就给lst1这个变量重新赋予了一个地址,覆盖了原本列表的地址,这个时候,lst1列表的内存id就发生了改变。上面这个道理用在所有复杂的数据类型中都是一样的。

  9. 变量的赋值

  10.   搞明白了上面的内容,再来探讨变量的赋值,就变得非常简单了。

  11.   1.str的赋值

  12.         

  13.   我们刚刚已经知道,str1的再次初始化(赋值)会导致内存地址的改变,从上图的结果我们可以看出修改了str1之后,被赋值的str2从内存地址到值都没有受到影响。

  14.   看内存中的变化,起始的赋值操作让str1和str2变量都存储了‘helloworld’所在的地址,重新对str1初始化,使str1中存储的地址发生了改变,指向了新建的值,此时str2变量存储的内存地址并未改变,所以不受影响。

  15.   2.复杂的数据结构中的赋值

  16.   刚刚我们看了简单数据类型的赋值,现在来看复杂数据结构变化对应内存的影响。

  17.        

  18.   上图对列表的增加修改操作,没有改变列表的内存地址,lst1和lst2都发生了变化。

  19.   对照内存图我们不难看出,在列表中添加新值时,列表中又多存储了一个新元素的地址,而列表本身的地址没有变化,所以lst1和lst2的id均没有改变并且都被添加了一个新的元素。

  20.   简单的比喻一下,我们出去吃饭,lst1和lst2就像是同桌吃饭的两个人,两个人公用一张桌子,只要桌子不变,桌子上的菜发生了变化两个人是共同感受的。

  21. 初识拷贝

  22.   我们已经详细了解了变量赋值的过程。对于复杂的数据结构来说,赋值就等于完全共享了资源,一个值的改变会完全被另一个值共享。

  23.   然而有的时候,我们偏偏需要将一份数据的原始内容保留一份,再去处理数据,这个时候使用赋值就不够明智了。python为这种需求提供了copy模块。提供了两种主要的copy方法,一种是普通的copy,另一种是deepcopy。我们称前者是浅拷贝,后者为深拷贝。

  24.   深浅拷贝一直是所有编程语言的重要知识点,下面我们就从内存的角度来分析一下两者的区别。

  25. 浅拷贝

  26.   首先,我们来了解一下浅拷贝。浅拷贝:不管多么复杂的数据结构,浅拷贝都只会copy一层。下面就让我们看一张图,来了解一下浅浅拷贝的概念。

  27.        

  28.    看上面两张图,我们加入左图表示的是一个列表sourcelist,sourcelist= [‘str1’,’str2’,’str3’,’str4’,’str5’,[‘str1’,’str2’,’str3’,’str4’,’str5’]];

  29.   右图在原有的基础上多出了一个浅拷贝的copylist,copylist= [‘str1’,’str2’,’str3’,’str4’,’str5’,[‘str1’,’str2’,’str3’,’str4’,’str5’]];

  30.   sourcelist和copylist表面上看起来一模一样,但是实际上在内存中已经生成了一个新列表,copy了sourceLst,获得了一个新列表,存储了5个字符串和一个列表所在内存的地址。

  31.    我们看下面分别对两个列表进行的操作,红色的框框里面是变量初始化,初始化了上面的两个列表;我们可以分别对这两个列表进行操作,例如插入一个值,我们会发现什么呢?如下所示:

        从上面的代码我们可以看出,对于sourceLst和copyLst列表添加一个元素,这两个列表好像是独立的一样都分别发生了变化,但是当我修改lst的时候,这两个列表都发生了变化,这是为什么呢?我们就来看一张内存中的变化图:

     

  我们可以知道sourceLst和copyLst列表中都存储了一坨地址,当我们修改了sourceLst1的元素时,相当于用’sourceChange’的地址替换了原来’str1’的地址,所以sourceLst的第一个元素发生了变化。而copyLst还是存储了str1的地址,所以copyLst不会发生改变。

  当sourceLst列表发生变化,copyLst中存储的lst内存地址没有改变,所以当lst发生改变的时候,sourceLst和copyLst两个列表就都发生了改变。

  这种情况发生在字典套字典、列表套字典、字典套列表,列表套列表,以及各种复杂数据结构的嵌套中,所以当我们的数据类型很复杂的时候,用copy去进行浅拷贝就要非常小心。。。

深拷贝

  刚刚我们了解了浅拷贝的意义,但是在写程序的时候,我们就是希望复杂的数据结构之间完全copy一份并且它们之间又没有一毛钱关系,应该怎么办呢?

  我们引入一个深拷贝的概念,深拷贝——即python的copy模块提供的另一个deepcopy方法。深拷贝会完全复制原变量相关的所有数据,在内存中生成一套完全一样的内容,在这个过程中我们对这两个变量中的一个进行任意修改都不会影响其他变量。下面我们就来试验一下。

      

   看上面的执行结果,这一次我们不管是对直接对列表进行操作还是对列表内嵌套的其他数据结构操作,都不会产生拷贝的列表受影响的情况。我们再来看看这些变量在内存中的状况:

    

 

  看了上面的内容,我们就知道了深拷贝的原理。其实深拷贝就是在内存中重新开辟一块空间,不管数据结构多么复杂,只要遇到可能发生改变的数据类型,就重新开辟一块内存空间把内容复制下来,直到最后一层,不再有复杂的数据类型,就保持其原引用。这样,不管数据结构多么的复杂,数据之间的修改都不会相互影响。这就是深拷贝~~~

关于copy()和deepcopy()的第二篇博客

其实呢,copy()与deepcopy()之间的区分必须要涉及到python对于数据的存储方式。


首先直接上结论:

—–我们寻常意义的复制就是深复制,即将被复制对象完全再复制一遍作为独立的新个体单独存在。所以改变原有被复制对象不会对已经复制出来的新对象产生影响。

—–而浅复制并不会产生一个独立的对象单独存在,他只是将原有的数据块打上一个新标签,所以当其中一个标签被改变的时候,数据块就会发生变化,另一个标签也会随之改变。这就和我们寻常意义上的复制有所不同了。

对于简单的object,用shallowcopy和deepcopy没区别复杂的object,如list中套着list的情况,shallowcopy中的子list,并未从原object真的「独立」出来。也就是说,如果你改变原object的子list中的一个元素,你的copy就会跟着一起变。这跟我们直觉上对「复制」的理解不同。


看不懂文字没关系我们来看代码:

>>>importcopy

>>>origin=[1,2,[3,4]]

#origin里边有三个元素:1,2,[3,4]

>>>cop1=copy.copy(origin)

>>>cop2=copy.deepcopy(origin)

>>>cop1==cop2

True

>>>cop1iscop2

False

#cop1和cop2看上去相同,但已不再是同一个object

>>>origin[2][0]="hey!"

>>>origin

[1,2,['hey!',4]]

>>>cop1

[1,2,['hey!',4]]

>>>cop2

[1,2,[3,4]]

#把origin内的子list[3,4]改掉了一个元素,观察cop1和cop2

可以看到cop1,也就是shallowcopy跟着origin改变了。而cop2,也就是deepcopy并没有变。

似乎deepcopy更加符合我们对「复制」的直觉定义:一旦复制出来了,就应该是独立的了。如果我们想要的是一个字面意义的「copy」,那就直接用deep_copy即可。

那么为什么会有shallowcopy这样的「假」copy存在呢?这就是有意思的地方了。

python的数据存储方式

Python存储变量的方法跟其他OOP语言不同。它与其说是把值赋给变量,不如说是给变量建立了一个到具体值的reference。

当在Python中a=something应该理解为给something贴上了一个标签a。当再赋值给a的时候,就好象把a这个标签从原来的something上拿下来,贴到其他对象上,建立新的reference。这就解释了一些Python中可能遇到的诡异情况:

>>a=[1,2,3]

>>>b=a

>>>a=[4,5,6]//赋新的值给a

>>>a

[4,5,6]

>>>b

[1,2,3]

#a的值改变后,b并没有随着a变


>>>a=[1,2,3]

>>>b=a

>>>a[0],a[1],a[2]=4,5,6//改变原来list中的元素

>>>a

[4,5,6]

>>>b

[4,5,6]

#a的值改变后,b随着a变了

上面两段代码中,a的值都发生了变化。区别在于,第一段代码中是直接赋给了a新的值(从[1,2,3]变为[4,5,6]);而第二段则是把list中每个元素分别改变。

而对b的影响则是不同的,一个没有让b的值发生改变,另一个变了。怎么用上边的道理来解释这个诡异的不同呢?

首次把[1,2,3]看成一个物品。a=[1,2,3]就相当于给这个物品上贴上a这个标签。而b=a就是给这个物品又贴上了一个b的标签。


第一种情况:

a=[4,5,6]就相当于把a标签从[1,2,3]上撕下来,贴到了[4,5,6]上。

在这个过程中,[1,2,3]这个物品并没有消失。b自始至终都好好的贴在[1,2,3]上,既然这个reference也没有改变过。b的值自然不变。


第二种情况:

a[0],a[1],a[2]=4,5,6则是直接改变了[1,2,3]这个物品本身。把它内部的每一部分都重新改装了一下。内部改装完毕后,[1,2,3]本身变成了[4,5,6]。

而在此过程当中,a和b都没有动,他们还贴在那个物品上。因此自然ab的值都变成了[4,5,6]。

搞明白这个之后就要问了,对于一个复杂对象的浅copy,在copy的时候到底发生了什么?

再看一段代码:

>>>importcopy

>>>origin=[1,2,[3,4]]

#origin里边有三个元素:1,2,[3,4]

>>>cop1=copy.copy(origin)

>>>cop2=copy.deepcopy(origin)

>>>cop1==cop2

True

>>>cop1iscop2

False

#cop1和cop2看上去相同,但已不再是同一个object

>>>origin[2][0]="hey!"

>>>origin

[1,2,['hey!',4]]

>>>cop1

[1,2,['hey!',4]]

>>>cop2

[1,2,[3,4]]

#把origin内的子list[3,4]改掉了一个元素,观察cop1和cop2

学过docker的人应该对镜像这个概念不陌生,我们可以把镜像的概念套用在copy上面。

概念图如下:


copy对于一个复杂对象的子对象并不会完全复制,什么是复杂对象的子对象呢?就比如序列里的嵌套序列,字典里的嵌套序列等都是复杂对象的子对象。对于子对象,python会把它当作一个公共镜像存储起来,所有对他的复制都被当成一个引用,所以说当其中一个引用将镜像改变了之后另一个引用使用镜像的时候镜像已经被改变了。

所以说看这里的origin[2],也就是[3,4]这个list。根据shallowcopy的定义,在cop1[2]指向的是同一个list[3,4]。那么,如果这里我们改变了这个list,就会导致origin和cop1同时改变。这就是为什么上边origin[2][0]=“hey!”之后,cop1也随之变成了[1,2,[‘hey!’,4]]。

而deepcopy概念图如下:


deepcopy的时候会将复杂对象的每一层复制一个单独的个体出来。

这时候的origin[2]和cop2[2]虽然值都等于[3,4],但已经不是同一个list了。即我们寻常意义上的复制。



相关问题推荐

  • 回答 3

    换行。比如,print hello\nworld效果就是helloworld\n就是一个换行符。\是转义的意思,'\n'是换行,'\t'是tab,'\\'是,\ 是在编写程序中句子太长百,人为换行后加上\但print出来是一整行。...

  • 回答 42

    十种常见排序算法一般分为以下几种:(1)非线性时间比较类排序:a. 交换类排序(快速排序、冒泡排序)b. 插入类排序(简单插入排序、希尔排序)c. 选择类排序(简单选择排序、堆排序)d. 归并排序(二路归并排序、多路归并排序)(2)线性时间非比较类排序:...

  • 回答 70
    已采纳

    前景很好,中国正在产业升级,工业机器人和人工智能方面都会是强烈的热点,而且正好是在3~5年以后的时间。难度,肯定高,要求你有创新的思维能力,高数中的微积分、数列等等必须得非常好,软件编程(基础的应用最广泛的语言:C/C++)必须得很好,微电子(数字电...

  • 回答 28

    迭代器与生成器的区别:(1)生成器:生成器本质上就是一个函数,它记住了上一次返回时在函数体中的位置。对生成器函数的第二次(或第n次)调用,跳转到函数上一次挂起的位置。而且记录了程序执行的上下文。生成器不仅记住了它的数据状态,生成器还记住了程序...

  • 回答 9

    python中title( )属于python中字符串函数,返回’标题化‘的字符串,就是单词的开头为大写,其余为小写

  • 回答 6

    第一种解释:代码中的cnt是count的简称,一种电脑计算机内部的数学函数的名字,在Excel办公软件中计算参数列表中的数字项的个数;在数据库( sq| server或者access )中可以用来统计符合条件的数据条数。函数COUNT在计数时,将把数值型的数字计算进去;但是...

  • 回答 1

    head是方法,所以需要取小括号,即dataset.head()显示的则是前5行。data[:, :-1]和data[:, -1]。另外,如果想通过位置取数据,请使用iloc,即dataset.iloc[:, :-1]和dataset.iloc[:, -1],前者表示的是取所有行,但不包括最后一列的数据,结果是个DataFrame。...

  • Python入门简单吗2021-09-23 13:21
    回答 45

    挺简单的,其实课程内容没有我们想象的那么难、像我之前同学,完全零基础,培训了半年,直接出来就工作了,人家还在北京大公司上班,一个月15k,实力老厉害了

  • 回答 4

    Python针对众多的类型,提供了众多的内建函数来处理(内建是相对于导入import来说的,后面学习到包package时,将会介绍),这些内建函数功用在于其往往可对多种类型对象进行类似的操作,即多种类型对象的共有的操作;如果某种操作只对特殊的某一类对象可行,Pyt...

  • 回答 8

     相当于 ... 这里不是注释

  • 回答 4

    还有FIXME

  • 回答 3

    python的两个库:xlrd和xlutils。 xlrd打开excel,但是打开的excel并不能直接写入数据,需要用xlutils主要是复制一份出来,实现后续的写入功能。

  • 回答 8

    单行注释:Python中的单行注释一般是以#开头的,#右边的文字都会被当做解释说明的内容,不会被当做执行的程序。为了保证代码的可读性,一般会在#后面加一两个空格然后在编写解释内容。示例:#  单行注释print(hello world)注释可以放在代码上面也可以放在代...

  • 回答 2

    主要是按行读取,然后就是写出判断逻辑来勘测行是否为注视行,空行,编码行其他的:import linecachefile=open('3_2.txt','r')linecount=len(file.readlines())linecache.getline('3_2.txt',linecount)这样做的过程中发现一个问题,...

  • 回答 4

    或许是里面有没被注释的代码

  • 回答 26

    自学的话要看个人情况,可以先在B站找一下视频看一下

没有解决我的问题,去提问