在一些业务场景中, 有时候我们需要复制一个对象, 但是又不想对原来的对象产生影响, 就想搞个 副本 来为所欲为地操作嘛. 但是呢, 在 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. Esp32s3(立创实战派)移植LVGL

    Esp32s3(立创实战派)移植LVGL 移植: 使用软件EEZ studio 创建工程选择带EEZ Flow的,可以使用该软件更便捷的功能 根据屏幕像素调整画布为320*240 复制ui文件至工程 ...

  2. 利用JS 代码块 为你的 Typecho博客添加一个 Copy 按钮

    引入 JS 将以下代码添加到主题 header.php 中的 </head> 标签前,本主题也可以前往 控制台 - 设置外观 - 主题自定义扩展,将它添加到 自定义 HTML 元素拓展 - ...

  3. Linux - Centos操作系统iso文件下载

    CENTOS VERSION DOWNLOAD LINK CentOS 8.5(2111) Download CentOS 8.4(2105) Download CentOS 8.3(2011) Do ...

  4. 寻找旋转排序数组中的最小值 II

    地址:https://leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array-ii/ <?php /** 154. 寻找旋转排 ...

  5. 【由技及道】量子构建交响曲:Jenkinsfile流水线的十一维编程艺术【人工智障AI2077的开发日志008】

    摘要:当代码提交触发时空涟漪,当构建流水线穿越量子维度--欢迎来到自动化构建的终极形态.本文将揭示如何用Jenkinsfile编写量子构建乐章,让每次代码提交都成为跨维度交响乐的音符. 动机:构建系统 ...

  6. [第四章]ABAQUS CM插件中文手册

    ABAQUS Composite Modeler User Manual(zh-CN) Dassault Systèmes, 2018 注: 源文档的交叉引用链接,本文无效 有些语句英文表达更易理解, ...

  7. Docker Hub 镜像加速器——持续更新(2025年3月12日)

    国内从 Docker Hub 拉取镜像有时会遇到困难,此时可以配置镜像加速器.Docker 官方和国内很多云服务商都提供了国内加速器服务. 配置加速地址 Ubuntu 16.04+.Debian 8+ ...

  8. anaconda创建新环境

    博客地址:https://www.cnblogs.com/zylyehuo/ anaconda创建新环境 conda create -n 环境自定义的名称 python=版本号 anaconda指定路 ...

  9. http状态码413,并提示Request Entity Too Large的解决办法

    使用wordpress的用户经常遇到的问题,就是在后台上传多媒体文件的时候,发现文件大小是有限制的,通常是2M.如图: 如果上传的文件超过2M,服务端返回的状态码会是413,同时提示上传失败.实际上, ...

  10. 什么是集群&集群的分类

    集群(Cluster)    计算机集群简称集群,是一种计算机系统,它通过一组松散集成的计算机软件(和/或)硬件连接起来高度紧密地协作完成计算工作.在某种意义上,他们可以被看作是一台计算机.集群系统中 ...