热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【Python】详解可变/不可变对象与深/浅拷贝

目录一、绪论二、说明2.1赋值(Assignment)2.1.1变量与对象(VariablesandObjects)2.1.2不可变对象(ImmutableObjects

目录

一、绪论

二、说明

2.1 赋值 (Assignment)

2.1.1 变量与对象 (Variables and Objects)

2.1.2 不可变对象 (Immutable Objects)

2.1.3 可变对象 (Mutable Objects)

2.1.4 直接赋值 (Direct Assignment)

2.2 copy.copy() —— 浅拷贝 (Shallow Copy)

2.3 copy.deepcopy() —— 深拷贝 (Deep Copy)

2.4 其他 (Others)



一、绪论

        copy 模块 定义了对象拷贝相关方法。有别于使用 等号 “=” 赋值 的操作,copy 模块能够实现对数据对象的深、浅拷贝:

名称功能
copy()返回数据对象的 浅拷贝
deepcopy()返回数据对象的 深拷贝

        以下将结合 Python 等号赋值对比说明 深、浅拷贝 的作用和意义。但此前,须先理清 可变对象 & 不可变对象 的含义与联系。



二、说明

2.1 赋值 (Assignment)


2.1.1 变量与对象 (Variables and Objects)

        对象 指的是内存中存储数据的实体,具有明确的类型,在 Python 中一切都是对象,包括函数。

        变量  作为对象的 引用/别名,实质保存着所指对象的 内存地址

知识点:

        Python 是一门 动态 (dynamic) 强类型 (strong) 语言。动态 类型语言即 在运行期间才确定数据类型。例如,Vbscript 和 Python 是动态类型的,因为它们是 在赋值时确定变量的类型。相反,静态 类型语言 在编译期间就确定数据类型,这类语言大都通过要求 在使用任一变量前声明其数据类型 来确保类型固定,例如 Java 和 C。

>>> x = 666 # 666 是一个对象, 而 x 是指向对象 666 的一个变量, 类型相应为 int 型
>>> x
666## 变量 x 可以指向任意对象, 而没有类型的前提限制, 因为动态语言变量类型可随着赋值而动态改变>>> x = '666' # 变量 x 指向新的对象 '666', 类型随之变为 string 型
>>> x
'666'

        总之,在 Python 中,类型属于对象,变量本无类型,仅仅是一个对对象的引用。而 变量指向对象的数据类型若发生变化,则变量的类型亦随之改变。而赋值语句改变的是变量所执的对对象的引用,故一个变量可指向各种数据类型的对象

        此外,在 Python 中,从数据类型的角度看,对象可分为 “可变对象” 和 “不可变对象”,常见的内建类型有:



2.1.2 不可变对象 (Immutable Objects)

        不可变对象:对象相应内存中的值 不可改变,常见的有 int、float、string、tuple 等类型的对象。因为 Python 中的变量存放的是 对象引用,所以对不可变对象而言,尽管对象本身不可改变,但 变量对对象的引用或指向关系仍是可变的。具体而言,指向原不可变对象的变量被改变为指向新对象时,Python 会开辟一块新的内存区域,并令变量指向这个新内存 (存放新对象引用),因此 变量对对象的引用或指向关系是灵活的、可变的。例如:

i = 73 # 变量 i 指向原不可变对象 73 (变量 i 存放原对象 73 的引用)
i += 2 # 变量 i 指向新对象 75 (变量 i 存放原对象 75 的引用)

        综上可知,不可变对象自身并未改变,而是创建了新不可变对象,改变了变量的对象引用。具体而言,原不可变对象 73 内存中的值并未改变,Python 创建了新不可变对象 75,并令变量 i 重新指向新不可变对象 75 / 保存对新对象 75 的引用,并通过 “垃圾回收机制” 回收原对象 73 的内存。

知识点:

  • 垃圾回收 (garbage collection) 机制指:对处理完毕后不再需要的堆内存空间的数据对象 (“垃圾”) 进行清理,释放它们所使用的内存空间的过程。例如,C 使用 free() 函数;C++ 使用 delete 运算符;而在 C++ 基础上开发的 C# 和 Java 等,其程序运行环境会自动进行垃圾回收,以避免用户疏忽而忘记释放内存,造成 内存泄露 (memory leaky) 问题。
  • Python 通过 引用计数 (Reference Counting) 和一个 能够检测和打破循环引用的循环垃圾回收器 来执行垃圾回收。可用 gc 模块 控制垃圾回收器。具体而言,对每个对象维护一个 ob_refcnt 字段 (对象引用计数器),用于记录该对象当前被引用的次数。每当有新引用指向该对象时,该对象的引用计数 ob_refcnt +1;每当该对象的引用失效时,该对象的引用计数 ob_refcnt -1;一旦对象的引用计数 ob_refcnt = 0,该对象立即被回收,对象占用的内存空间将被自动放入 自由内存空间池,以待后用。
  • 这种引用计数垃圾回收机制的 优点 在于,能够自动清理不用的内存空间,甚至能够随意新建对象引用 (不建议) 而无需考虑手动释放内存空间的问题,故相比于 C 或 C++ 这类静态语言更“省心”。
  • 这种引用计数垃圾回收机制的 次要缺点 是需要额外空间资源维护引用计数,主要缺点则是无法解决对象的“循环引用”问题。因此,也有很多语言如 Java 并未采用该机制。

        注意,对于不可变对象,所有指向该对象的变量在内存中 共用同一个地址这种多个变量引用同一个对象的现象叫做 共享引用但不管有多少个引用指向它,都只有一个地址值,只有一个引用计数会记录指向该地址的引用数目。

>>> x = 0
>>> y = 0
>>> print(id(x) == id(y))
True
>>> print(x is y)
True
>>> print(id(0), id(x), id(y)) # 结果不唯一, 但一定是相同的
2424416677616 2424416677616 2424416677616

        事实上,Python 对不可变对象有着许多性能/效率优化机制,若学有余力或饶有兴趣,不妨了解一下以加深对内存优化机制的理解,详见文章《【Python】详解 小整数池 & intern 机制 (不可变对象的内存优化原理) 》。 



2.1.3 可变对象 (Mutable Objects)

        可变对象:变量所指向对象的内存地址处的值 可改变,常见的有 list、set、dict 等类型的对象。因此指向可变对象的变量若发生改变,则该可变对象亦随之改变,即发生 原地 (in-place) 修改。另一方面, 当可变对象相应内存中的值变化时,变量的对可变对象引用仍保持不变,即变量仍指向原可变对象。例如:

>>> m = [5, 9] # 变量 m 指向可变对象 (list)
>>> id(m)
1841032547080>>> m += [6] # 可变对象 (list) 将随变量 m 的改变而发生原地 (in-place) 修改, 但 m 仍是其引用 (保存的内存地址 id 不变)
>>> id(m)
1841032547080

        综上可知,可变对象随着变量的改变而改变,但变量对可变对象的引用关系仍保持不变,即变量仍指向原可变对象。例如,变量 m 先指向可变对象 [5, 9] ,然后随着变量增加元素 6,可变对象 [5, 9] 也随之在内存中增加 6,而变化前、后变量 m 始终指向同一个可变对象 / 保存对同一可变对象的引用。 

        但注意,我们也由此知道,对于 “看起来相同” 的可变对象,其内存地址是完全不同的,例如:

>>> n = [1, 2, 3]
>>> id(n)
1683653539464>>> n = [1, 2, 3]
>>> id(n)
1683653609928

        可见,对于两个可变对象 [1, 2, 3],二者是先后分别创建的新可变对象,虽然值相同,但内存地址完全不同。而这点有别于不可变对象,因为 所有指向不可变对象的变量在内存中共用同一个地址 (比如 2.1.2 中 666 的例子)。



2.1.4 直接赋值 (Direct Assignment)

        Python 中的变量存在 深拷贝 浅拷贝 的 区别:

  • 对于不可变对象,无论深、浅拷贝,内存地址 (id) 都是一成不变的;
  • 对于可变对象,则存在 3 种不同情况。

        以下以 list 为例简要说明 可变对象的 3 种情况:

        情况一 - 直接赋值:仅拷贝了对可变对象的引用,故前后变量均未隔离,任一变量 / 对象改变,则所有引用了同一可变对象的变量都作相同改变。例如:

>>> x = [555, 666, [555, 666]]
>>> y = x # 直接赋值, 变量前后并未隔离
>>> y
[555, 666, [555, 666]]# 修改变量 x, 变量 y 也随之改变
>>> x.append(777)
>>> x
[555, 666, [555, 666], 777]
>>> y
[555, 666, [555, 666], 777]# 修改变量 y, 变量 x 也随之改变
>>> y.pop()
777
>>> y
[555, 666, [555, 666]]
>>> x
[555, 666, [555, 666]]

        在某些情况下,这是致命的,因此还需要深、浅拷贝来正确实现真正所需的拷贝目的。



2.2 copy.copy() —— 浅拷贝 (Shallow Copy)

        情况二 - 浅拷贝:使用 copy(x) 函数,拷贝可变对象如 list 的最外层对象并实现隔离,但 list 内部的嵌套对象仍是未被隔离的引用关系。例如:

>>> import copy
>>> x = [555, 666, [555, 666]]
>>> z = copy.copy(x) # 浅拷贝
>>> zz = x[:] # 也是浅拷贝, 等同于使用 copy() 函数的 z
>>> z
[555, 666, [555, 666]]
>>> zz
[555, 666, [555, 666]]# 改变变量 x 的外围元素, 不会改变浅拷贝变量
>>> x.append(777)
>>> x
[555, 666, [555, 666], 777] # 只有自身改变, 增加了外围元素 777
>>> z
[555, 666, [555, 666]] # 未改变
>>> zz
[555, 666, [555, 666]] # 未改变# 改变变量 x 的内层元素, 则会改变浅拷贝变量
>>> x[2].append(888)
>>> x
[555, 666, [555, 666, 888], 777] # 同时发生改变, 增加了内层元素 888
>>> z
[555, 666, [555, 666, 888]] # 同时发生改变, 增加了内层元素 888
>>> zz
[555, 666, [555, 666, 888]] # 同时发生改变, 增加了内层元素 888# 浅拷贝变量的外围元素改变不会相互影响
>>> z.pop(0)
555
>>> x
[555, 666, [555, 666, 888], 777] # 未改变
>>> z
[666, [555, 666, 888]] # 只有自身改变, 弹出了外围元素 555
>>> zz
[555, 666, [555, 666, 888]] # 未改变# 浅拷贝变量的内层元素改变会相互影响
>>> z[1].pop()
888
>>> x
[555, 666, [555, 666], 777] # 同时发生改变, 弹出了内层元素 888
>>> z
[666, [555, 666]] # 同时发生改变, 弹出了内层元素 888
>>> zz
[555, 666, [555, 666]] # 同时发生改变, 弹出了内层元素 888

        注意,所谓改变应包含 “增、删、改” 三种,以上仅展示了前两种情况,第三种不言自明。

        此外,若有人问元组 (tuple) 一定是不可变的吗?答案是不一定,因为浅拷贝时仅隔离最外层对象,而内层嵌套对象则仍为引用关系,例如:

>>> t = (1, 2, [3, 4]) # tuple
>>> import copy
>>> ct = copy.copy(t) # 浅拷贝 tuple # 注意, 令 ct = t 时此例结果仍然相同
>>> ct
(1, 2, [3, 4])
>>> ct[2][-1] = 5 # 修改 ct
>>> ct
(1, 2, [3, 5])
>>> t # t 也随之改变, 证明内层嵌套对象仍为引用关系
(1, 2, [3, 5])



2.3 copy.deepcopy() —— 深拷贝 (Deep Copy)

        情况三 - 深拷贝:使用 deepcopy(x[,memo]) 函数,拷贝可变对象如 list 的“外围+内层”而非引用,实现对前后变量不论深浅层的完全隔离。例如:

>>> import copy
>>> x = [555, 666, [555, 666]]
>>> k = copy.deepcopy(x) # 深拷贝
>>> k
[555, 666, [555, 666]]# 改变变量 x 的外围元素, 不会改变深拷贝变量
>>> x.append(777)
>>> x
[555, 666, [555, 666], 777]
>>> k
[555, 666, [555, 666]] # 未改变# 改变变量 x 的内层元素, 同样不会改变深拷贝变量
>>> x[2].append(888)
>>> x
[555, 666, [555, 666, 888], 777]
>>> k
[555, 666, [555, 666]] # 未改变# 深拷贝变量的外围元素改变不会相互影响
>>> k.pop(0)
555
>>> x
[555, 666, [555, 666, 888], 777] # 未改变
>>> k
[666, [555, 666]]# 深拷贝变量的内层元素改变同样不会相互影响
>>> k[1].pop()
666
>>> x
[555, 666, [555, 666, 888], 777] # 未改变
>>> k
[666, [555]]

        再次试验元组 (tuple) 的例子以展示浅拷贝和深拷贝的区别于联系:

>>> t = (1, 2, [3, 4]) # tuple
>>> import copy
>>> ct = copy.deepcopy(t) # 深拷贝 tuple
>>> ct
(1, 2, [3, 4])
>>> ct[2][-1] = 5 # ct 改变
>>> ct
(1, 2, [3, 5])
>>> t # t 不论外层还是内层嵌套变量, 均不变 (完全隔离)
(1, 2, [3, 4])



2.4 其他 (Others)

        上述内容即为基本用法,对于普通使用足够了。若想进一步深入,可选读如下内容:

        浅拷贝和深拷贝之间的区别仅在于 复合对象 (即包含其他对象的对象,如 list 或类的实例) 相关:

  • 一个 浅拷贝 会构造一个新的复合对象,然后 (在可能的范围内) 将原对象中找到的 引用  插入其中。

  • 一个 深拷贝 会构造一个新的复合对象,然后递归地将原始对象中所找到的对象的 副本 插入。


        深拷贝操作通常存在两个问题,而浅拷贝操作并不存在这些问题:

  • 递归对象 (直接或间接包含对自身引用的复合对象) 可能会导致 递归循环

  • 由于深拷贝会复制所有内容 (外围内层),故可能 过多复制 (例如本应在副本间共享的数据) 。


        深拷贝函数 deepcopy() 通过以下方式避免上述问题:

  • 保留在当前复制过程中已复制的对象的 “备忘录” (memo) 字典;

  • 允许用户定义的类重载复制操作或复制的组件集合。


        此外,copy 模块不拷贝模块、方法、栈追踪(stack trace)、栈帧(stack frame)、文件、套接字、窗口、数组及任何类似的类型。它通过不改变地返回原始对象来(浅层或深层地)“复制” 函数和类;类似于 pickle 模块处理这类问题的方式。


参考资料:

Python可变对象和不可变对象

8.10. copy — 浅层 (shallow) 和深层 (deep) 复制操作 — Python 3.6.15 文档

Python中的垃圾回收机制(转) - 奋斗终生 - 博客园


推荐阅读
  • 本文介绍了在Python3中如何使用选择文件对话框的格式打开和保存图片的方法。通过使用tkinter库中的filedialog模块的asksaveasfilename和askopenfilename函数,可以方便地选择要打开或保存的图片文件,并进行相关操作。具体的代码示例和操作步骤也被提供。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • 本文介绍了如何使用python从列表中删除所有的零,并将结果以列表形式输出,同时提供了示例格式。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 本文介绍了Python对Excel文件的读取方法,包括模块的安装和使用。通过安装xlrd、xlwt、xlutils、pyExcelerator等模块,可以实现对Excel文件的读取和处理。具体的读取方法包括打开excel文件、抓取所有sheet的名称、定位到指定的表单等。本文提供了两种定位表单的方式,并给出了相应的代码示例。 ... [详细]
  • 本文介绍了lua语言中闭包的特性及其在模式匹配、日期处理、编译和模块化等方面的应用。lua中的闭包是严格遵循词法定界的第一类值,函数可以作为变量自由传递,也可以作为参数传递给其他函数。这些特性使得lua语言具有极大的灵活性,为程序开发带来了便利。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 摘要: 在测试数据中,生成中文姓名是一个常见的需求。本文介绍了使用C#编写的随机生成中文姓名的方法,并分享了相关代码。作者欢迎读者提出意见和建议。 ... [详细]
  • 本文介绍了iOS数据库Sqlite的SQL语句分类和常见约束关键字。SQL语句分为DDL、DML和DQL三种类型,其中DDL语句用于定义、删除和修改数据表,关键字包括create、drop和alter。常见约束关键字包括if not exists、if exists、primary key、autoincrement、not null和default。此外,还介绍了常见的数据库数据类型,包括integer、text和real。 ... [详细]
author-avatar
顺佳海外
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有