在一些业务场景中, 有时候我们需要复制一个对象, 但是又不想对原来的对象产生影响, 就想搞个 副本 来为所欲为地操作嘛. 但是呢, 在 Python中呢, 又不能通过 赋值 的方式达到效果, 为啥呢? 被坑过几次就明白了, 这里面蕴含有很多的学问呀, 涉及 Python 变量的本质, 可变与不变对象, 深浅拷贝问题... 是得来总结一波了.

副本危机

之前在做数据分析的时候, 想把一个 DataFrame 对象, 引用的是一张Excel表数据, 我当时想拷贝一个副本来瞎几把操作, 而想着不会改变原来的表. 然后就打脸了, 情形大致模拟一下.

首先我还是先测试了一波

>>> a = 123  # a -> 123
>>> b = a # b -> a -> 123
>>> a = 245 # a -> 245
>>> b # b 不受 a 影响
123

这感觉没啥问题的, 于是我就做了如下的操作.

>>> df = pd.DataFrame({'a':[1,2], 'b':[3,4]})
>>> df
a b
0 1 3
1 2 4
>>> df2 = df # df2 作为副本 >>> df['c']=666 # 新增一列
>>> df
a b c
0 1 3 666
1 2 4 666 >>> df2
a b c
0 1 3 666
1 2 4 666 # ??? 卧槽, 副本也跟着变了, 可怕...

然后就翻车了. 以为的副本, 竟然跟着变了 这尤其是在数据分析中, 副本没了, 我只能重新花大量时间重读数据 , 非常难受. 这是我真正的副本危机...

Python 对象的可变性

变量 is 指针

Python中有一个说法是 万物皆对象, 抛开类, 实例这一块来说, 这也逐渐揭示了, Python 中, 变量的本质是一个指针 因而变量可以不用声明类型, 直接指向该实例对象即可, 因为Python变量压根就不存储值, 而是对象的引用地址.

# 理解Python 中的 "=" 不是赋值, 是 "指向"(地址)
>>> a = 123 # a -> 123
>>> b = a # b -> a -> 123 => b -> 123
>>> c = b # c->b->a->123 => c -> 123 >>> id(a)
1457811488
>>> id(b)
1457811488
>>> id(c)
1457811488 # 现在让 a -> 456, 但不会影响 b, c 的
>>> a = 456
>>> id(a)
1993350015664 # a 变了, b, c 的不会变的, 还是123这个对象地址
>>> id(b)
1457811488
>>> b
123
>>> id(c)
1457811488

这个是 Python变量的本质, 是指针, 真正理解这一步非常重要, " = " 不是赋值, 是地址引用哦.

不可变对象

不可变对象, 即对于该变量指向的对象而言, 如果 修改了对象的值, 就相对于重新实例化了一个新对象, 则指向的地址也就变了. 通俗就是, 一旦对象的值改变了, 那指向的地址也就变了.

这其实, 正是我想要的副本效果呀. 但在Python中, 有些是改变了, 有些是没改变, 头疼...

# 数值 是不可变类型
>>> a = 123
>>> b = a
>>> id(a) == id(b)
True
>>> a = 456
>>> id(a) == id(b)
False

从实践效果来看, Python中的不可变对象有: 字符串, 数值, 元组.

即在对不可变类型的对象进行操作时, 它会返回一个 新的对象 需要用新的变量去引用它. 而不改变本身. 这点在 Pandas 经常出现一个参数 Inplace = False 这样一个 是否原地修改的概念, 是一样的.

>>> s1 = 'abc'
>>> s2 = s1 # 对不可变类型 对象进行操作, 需要有 新变量 进行接收
>>> s1.lower()
'abc' # 不接收, 原对象 还是 原对象
>>> id(s1) == id(s2)
True

再重复一下, Python 针对不可变类型, 如数值, 字符串,元组而已, 一旦改变了对象的值, 就需要用新的变量来指向该新的地址. 或者这样说, 嗯, 一旦修改了值, 就相等于把该对象的值 复制 出来一份, 然后修改了值后, 给存到 令一个地址上去了.

这不就是我想要的副本效果呀.

可变对象

可变对象是指, 对一个对象进行修改其值, 不会改变, 该变量的引用地址. 跟上面的不可变对象是相反的. 一旦修改了值, 那就只是修改了值呀, 而没有存到新的地址上.

这样一来看, 是不是显得, inplace 这个词语非常直观呀. 就在原地就给修改了, 这样做的好处在于, 不用重新开辟一块新空间来存储, 而不好的地方也非常明显, 其他引用该地址的变量, 也跟着改变了 .

从实践经验来看, Python中的可变对象有 ~不可变类型 , 这样是不是很机智, 列表, 字典, ....

于是, 这就是我想的副本效果, 失败的原因.

# 以list为例

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

>>> b = a  # b->a

>>> id(b) == id(a)
True # 现在对该对象进行值的修改
>>> a.append(4)
>>> a
[1, 2, 3, 4] >>> id(a)
1993350498312
>>> id(b)
1993350498312 # 本想用b来存储 [1,2,3]作为副本, 结果也跟着变了.
>>> b
[1, 2, 3, 4]

这样一来, 你会发现, Python 的这种设计还是比较灵活的, 一开始觉得有点反人类, 但慢慢理解其设计内涵后, 会发现, 这样的灵活选取, 是真滴香.

深 - 浅 Copy 问题

回到本篇最初的问题, 无非是想要 搞一个对象的副本来瞎几把操作, 这个问题的本质不就是 对象拷贝呀.

于是呢, 在对于搞副本的过程中, 对于不可变对象而已, 直接 另起一个变量 来指向就好了. 而对于不可变类型来说, Python中是没有 赋值 操作的, 但又想达到该效果, 这就值得讨论了.

对于对象的拷贝, 想必在理解上差不多了, 现在是对于拷贝的程度, 是 是深还是浅 的问题, 怎么感觉像在开车 ???

深拷贝

这个比较好理解, 就差将对象的值 (不论其可不可变) 都拷贝一份作为真正的 副本. 这个副本跟原本是没有任何关系的了, 完全不受其他因素影响, 我就是我, 颜色不一样的烟火. 嗯...或者说, 二者彻底分手了, 不会再有藕断丝连.

理解深拷贝可能带来的2个问题 (关键词) : "copy everything", "recursive loop" 不过我工作中重来没有遇到过类似问题, 直接用就好了.

# 对于不可变对象的 深拷贝, 地址还是其自身
# 功效类似于 a = 123, b = a, 没有copy一说嘛 # 对于可变对象的深拷贝, 地址都不一样
>>> import copy # 可变对象
>>> a = [1,2,3]
>>> b = copy.deepcopy(a) >>> print("t1:", id(a), id(b))
t1: 1993350498312 1993350498696 >>> a.append(4) >>> print("t1:", id(a), id(b))
t1: 1993350498312 1993350498696 >>> print(b)
[1, 2, 3]
>>> print(a)
[1, 2, 3, 4]

浅拷贝

对于不可变对象来说, 不存在拷贝这一说, 本身就是唯一.

在浅拷贝时, 拷贝出的新对象, 地址不一样, 但里面的结构中, 元素的地址还是没有变的. 即: 浅拷贝只是拷贝了个外壳, 里面的可变元素的地址, 并没有发生改变. 这就是, 藕断丝连呀, 表面分手, 然后还是地下情不断.. ....

# 这是可变对象的 正常操作
>>> a = [1,2,3]
>>> b = a
>>> id(a) == id(b)
True
>>> a.append(3)
>>> b
[1, 2, 3, 3]
>>> a
[1, 2, 3, 3]

然后来看看浅拷贝, 只拷贝外层

>>> import copy
>>> a = [1,2,3]
>>> b = copy.copy(a) # 浅拷贝, 外层的地址是会改变的
>>> id(a) == id(b)
False # 里面的元素, 还是原来的, 并没有跟着拷贝 >>> id(a[0]) == id(b[0])
True
>>> a.append(3)
>>> b
[1, 2, 3]

浅拷贝 - 内层有可变元素时, 会互相影响的哦

>>> a = [1,2, [3,4]]
>>> b = copy.copy(a) # 外层copy, 地址不同, 没问题
>>> id(a) == id(b)
False # 里面的元素,并没有copy 还是引用
>>> id(a[1]) == id(b[1])
True
>>> id(a[2]) == id(b[2])
True # 一旦改变里面的 可变对象时,
# 浅拷贝的对象中相应的元素也会发生变化, 这就是表面分手,实际地下情
>>> a[2].append(3)
>>> a
[1, 2, [3, 4, 3]] >>> b
[1, 2, [3, 4, 3]]

这就是, 浅拷贝的特点. 尤其要注意区分哦.

嗯, 另外补充一点关于常用的 列表切片拷贝, 它其实是 浅拷贝, 用的时候特别需要注意哦.

>>> lst1 = [1, [2,3]]
>>> lst2 = copy.copy(lst1) >>> id(lst1[1]) == id(lst2[1])
True # 对里层的,可变元素进行修改, 会影响另外的哦
>>> lst1[1].append(3)
>>> lst1
[1, [2, 3, 3]] >>> lst2
[1, [2, 3, 3]]

小结

  • 对象的深浅拷贝,在数据分析中是一个重要问题, 曾经踩过坑

  • Python中变量的本质是指针, 而万物皆对象的对象分为, 可变和不可变 (能不能 原地修改 还是需要新变量接收)

  • 问题都是出在 整副本 的过程, 副本就是深拷贝, 完全复一个新的对象, 地址也不同了, 跟原配彻底分手

  • 不可变对象 不存在深浅拷贝一说, 是唯一的, 只有引用. 像, 字符串, 数字, 元组.

  • 浅拷贝, 虽然外层地址变了, 换了个对象, 但里面的元素, 还是原来的引用, 还是藕断丝连的哦, 即里面的元素如果是可变类型的, 一个改变了, 另外的也会受影响的哦.

  • 列表的切片, 是 浅拷贝. 也是之前被坑过. 还以为是找了个新对象, 没想到, 是我太天真了...

Python 深拷贝 vs 浅拷贝的更多相关文章

  1. 浅显直白的Python深拷贝与浅拷贝区别说明

    一.可变数据类型与不可变数据类型 在开始说深拷贝与浅拷贝前,我们先来弄清楚,可变对象与不可变对象 总的来说,Python数据类型可分为可变数据类型与不可变数据类型 可变数据类型:在不改变对象所指向的地 ...

  2. python 深拷贝与浅拷贝

    浅拷贝的方式有: lst=[1,2,3] (1)直接赋值: lst_cp = lst (2)for循环遍历生成:lst_cp= [i for i in lst] (3)copy模块下,copy.cop ...

  3. Python深拷贝和浅拷贝

    1- Python引用计数[1] 1.1 引用计数机制 引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象.内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的 ...

  4. 【python测试开发栈】—理解python深拷贝与浅拷贝的区别

    内存的浅拷贝和深拷贝是面试时经常被问到的问题,如果不能理解其本质原理,有可能会答非所问,给面试官留下不好的印象.另外,理解浅拷贝和深拷贝的原理,还可以帮助我们理解Python内存机制.这篇文章将会通过 ...

  5. Python深拷贝与浅拷贝区别

    可变类型 如list.dict等类型,改变容器内的值,容器地址不变. 不可变类型 如元组.字符串,原则上不可改变值.如果要改变对象的值,是将对象指向的地址改变了 浅拷贝 对于可变对象来说,开辟新的内存 ...

  6. Python 深拷贝和浅拷贝的区别

    python的复制,深拷贝和浅拷贝的区别    在python中,对象赋值实际上是对象的引用.当创建一个对象,然后把它赋给另一个变量的时候,python并没有拷贝这个对象,而只是拷贝了这个对象的引用  ...

  7. python深拷贝和浅拷贝的区别

    首先深拷贝和浅拷贝都是对象的拷贝,都会生成一个看起来相同的对象,他们本质的区别是拷贝出来的对象的地址是否和原对象一样,也就是地址的复制还是值的复制的区别. 什么是可变对象,什么是不可变对象: 可变对象 ...

  8. Python——深拷贝和浅拷贝

    深拷贝.浅拷贝 1. 浅拷贝 浅拷贝是对于一个对象的顶层拷贝 import copy a = [[1, 2], 3] b = copy.copy(a) print(id(a)) print(id(b) ...

  9. python 深拷贝及浅拷贝区别

    深拷贝及浅拷贝区别 浅拷贝copy: 可变类型:(列表,字典,集合)copy函数对可变类型的第一层对象进行拷贝,对拷贝的对象开辟新的内存空间进行存储,不会拷贝对象内部的子对象 不可变类型:(数字,字符 ...

  10. PYTHON 深拷贝,浅拷贝

    声明:本篇笔记,模仿与其它博客中的内容 浅拷贝 浅拷贝,在内存中只额外创建第一层数据 import copy n1 = {"k1": "wu", "k ...

随机推荐

  1. [MQ] Kafka

    概述: Kafka 安装指南 安装 on Windows Step1 安装 JDK JDK 安装后: 在"系统变量"中,找到 JAVA_HOME,如果没有则新建,将其值设置为 JD ...

  2. Kubernetes - [02] 网络通讯方式

    题记部分 一.网络通讯模式   Kubernetes的网络模型假定了所有Pod都在一个可以直接连通的扁平的网络空间中,这在(GCEGoogle Compute Engine)里面是现成的网络模型,Ku ...

  3. 【渗透测试】 Vulnhub JANGOW: 1.0.1

    渗透环境 攻击机:   IP: 192.168.149.128(Kali) 靶机:     IP:192.168.149.129 靶机下载地址:https://www.vulnhub.com/entr ...

  4. Huawei Cloud EulerOS上安装sshpass

    下载源码 git clone https://github.com/kevinburke/sshpass.git 由于网络问题,这里我用了一个代理下载 git clone https://ghprox ...

  5. go ceph s3文件管理

    导入依赖 go get gopkg.in/amz.v1/aws go get gopkg.in/amz.v1/s3 创建用户 在初始化连接之前,我们需要创建一个用户得到accessKey和secret ...

  6. 安装卸载GNOME

    只需要三步:sudo yum -y groups install "GNOME Desktop"sudo systemctl set-default graphical.targe ...

  7. 【FAQ】HarmonyOS SDK 闭源开放能力 —Push Kit(11)

    1.问题描述: 鸿蒙push右侧图表没有正常展示. 解决方案: .jpg格式文件,头信息必须是这个"jpg:ffd8". 2.问题描述: 安卓端App在开发者平台申请了Androi ...

  8. [源码系列:手写spring] IOC第十一节:Aware接口

    内容介绍 Aware简洁 在Spring框架中,Aware接口是一个非常有用的工具,用于实现Bean与Spring容器及其他资源之间的集成.Aware接口是一个标记接口,其中定义了各种Aware子接口 ...

  9. 深入理解Hadoop读书笔记-3

    背景 公司的物流业务系统目前实现了使用storm集群进行过门事件的实时计算处理,但是还有一个需求,我们需要存储每个标签上传的每条明细数据,然后进行定期的标签报表统计,这个是目前的实时计算框架无法满足的 ...

  10. 继承中成员变量和成员方法的访问特点-java se进阶篇 day01

    1.继承中成员变量的访问特点 1.成员变量重名 如图 父类中有age变量,子类中也有age变量,这时打印age,出现的是10还是20呢? 答:根据就近原则,出现的是20 2.使用父类成员变量--sup ...