关于 Python 对象拷贝的那点事?
概述
在本篇文章中,会先介绍 Python 中对象的基础概念,之后会提到对象的深浅拷贝以及区别。在阅读后,应该掌握如下的内容:
理解变量、引用和对象的关系
理解 Python 对象中 identity,type 和 value 的概念
什么是 mutable 和 immutable 对象?以及它们和 hashable 的关系
深浅拷贝的过程以及区别
变量,引用和对象
变量无类型,它的作用仅仅在某个时候引用了特定的对象而已,具体在内存中就是一个指针,仅仅拥有指向对象的空间大小。
变量和对象的关系在于引用,变量引用对象后,也就对应了赋值的过程。
在 python 中一切皆为对象,具体在内存中表示一块内存空间,每一个对象都会具有 identity,type 和 value 这三个内容。
Identity, 一旦对象被创建后,Identity 的值便不会发生改变。在 Cpython 中,其值体现为内存中保存对象的地址。is
操作符,比较对象是否相等就是通过这个值。通过 id()
函数查看它的整数形式。
Type, 和 Identity 一样,在对象创建后,Type 也不会发生变化。它主要定义了一些可能支持的值和操作(如对列表来说,会有求长度的操作)。通过 type()
函数可以得到对象的类型。
Value,用于表示的某些对象的值。当对象在创建后值可以改变称为 mutable,否则的话被称为 immutable.
举个例子,比如在 C 中,int x = 4
在内存中,是先分配了一个 int 类型的内存空间,然后把 4 放进空间内。
而 Python 中,x = 4
正好相反,是为 4 分配了一块的内存空间,然后用 x 指向它。由于变量可以指向各种类型的对象,因此不需要像 C 一样声明变量。这也就是 Python 被称为动态类型的意义。
并且在 Python 中,变量可以删除,但对象是无法删除的。
immutable 和 mutable 对象
immutable 对象拥有一个固定的值,包括 numbers, strings, tuples. 一个新的值被保存时,一个新的对象就会被创建。这些对象在作为常量的 hash 值中有着非常重要的作用,如作为字典的 key 时。
mutable 对象可以改变自身的值,但 id()
并不会发生改变。
当一些对象包含对其他对象的一些引用时,我们称这些对象为 containers, 例如 list, tuple, dictionary 这些都是 containers. 这里需要注意的是,一个 immutable containers 可以包含对 mutable 对象的引用(如在 tuple 中包含一个 list)。 但这个对象仍然称为 immutable 对象,因为 Identity 是不变的。
hashable 对象
当一个对象在生命周期内(实现了 __hash__()
方法)hash 值不会发生改变,并可以与其他对象进行比较(实现了 __eq__()
方法),称之为hashable 对象。
在 Python 内置的 immutable 对象 大多数都是 hashable 对象。immutable containers(tuples, frozenset)在引用的对象都是 hashable 对象时,才是hashable 对象。mutable containers 容器都不是 hashable 对象。用户自定义的类都是 hashable 对象,
浅拷贝与深拷贝
在介绍对象的拷贝前,先介绍一下 Python 中的赋值操作,可以让我们更好的了解拷贝的过程。
赋值操作
赋值操作的右边是简单表达式:
def normal_operation():
# immutable objects
# int
a = 10
b = 10
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1777364320 , id of b: 1777364320
print(a == b) # True
print(a is b) # True
# str
str_a = '123'
str_b = '123'
print('----- str')
print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
# id of a:1615046978224 , id of b: 1615046978224
print(str_a == str_b) # True
print(str_a is str_b) # True
# tuple
tuple_a = (1, 2, 3)
tuple_b = (1, 2, 3)
print('----- tuple')
print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
# id of a:1615047009696 , id of b: 1615047024856
print(tuple_a == tuple_b) # True
print(tuple_a is tuple_b) # False
# mutable
# set
set_a = {1, 2, 3}
set_b = {1, 2, 3}
print('----- set')
print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
# id of a:1615045625000 , id of b: 1615047012872
print(set_a == set_b) # True
print(set_a is set_b) # False
# list
list_a = [1, 2, 3]
list_b = [1, 2, 3]
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:1615047017800 , id of b: 1615045537352
print(list_a == list_b) # True
print(list_a is list_b) # False
# dict
dict_a = {"name": "xxx", "age": "123"}
dict_b = {"name": "xxx", "age": "123"}
print('----- dict')
print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
# id of a:1615045521696 , id of b: 1615045522128
print(dict_a == dict_b) # True
print(dict_a is dict_b) # False
在 Cpython 中,id()
反映了对象在内存中的地址。可以看到,对于 immutable 对象中的 number 和 string 来说,CPython 本身对其做了一定的优化,在创建相同的内容时,使其 指向了相同的内存地址,从而被复用。
但是,Python 不会对所有 mutable 对象执行此操作,因为实现此功能需要一定的运行时成本。对于在内存中的对象来说,必须首先在内存中搜索对象(搜索意味着时间)。对于 number 和 string 来说,搜索到它们很容易,所以才对其做了这样的优化。
对于其他类型的对象,虽然创建的内容相同,但都在内存中完全创建了一块新的区域。
赋值操作的右边是 Python 中已存在的变量:
def assignment_operation():
# immutable objects
# int
a = 10
b = a
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1777364320 , id of b: 1777364320
print(a == b) # True
print(a is b) # True
# str
str_a = '123'
str_b = str_a
print('----- str')
print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
# id of a:2676110142128 , id of b: 2676110142128
print(str_a == str_b) # True
print(str_a is str_b) # True
# tuple
tuple_a = (1, 2, 3)
tuple_b = tuple_a
print('----- tuple')
print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
# id of a:2676110191640 , id of b: 2676110191640
print(tuple_a == tuple_b) # True
print(tuple_a is tuple_b) # True
# mutable
# set
set_a = {1, 2, 3}
set_b = set_a
print('----- set')
print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
# id of a:2676108788904 , id of b: 2676108788904
print(set_a == set_b) # True
print(set_a is set_b) # True
# list
list_a = [1, 2, 3]
list_b = list_a
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2676110181704 , id of b: 2676110181704
print(list_a == list_b) # True
print(list_a is list_b) # True
# dict
dict_a = {"name": "xxx", "age": "123"}
dict_b = dict_a
print('----- dict')
print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
# id of a:2676079063328 , id of b: 2676079063328
print(dict_a == dict_b) # True
print(dict_a is dict_b) # True
而当赋值操作的右边是已经存在的 Python 对象时,不论是什么类型的对象,都没有在内存中创建新的内容,仅仅是声明了一个新的变量指向之前内存中已经创建的对象,就像提供了一个别名一样。
改变赋值后的对象:
def assignment_operation_change():
# immutable objects
# int
a = 10
print("id of a:{}".format(id(a)))
# id of a:1994633728
b = a
a = a + 10
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1994634048 , id of b: 1994633728
print(a == b) # False
print(a is b) # False
# mutable objects
# list
list_a = [1, 2, 3]
list_b = list_a
list_a.append(4)
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2676110181704 , id of b: 2676110181704
print(list_a == list_b) # True
print(list_a is list_b) # True
当修改 imutable 对象时,由于其本身不可改变,只能在内存中新申请一块新的空间,用于存储修改后的内容。对应上面 a=20
的操作,这时再判断 a 和 b 时,由于指向了内存的不同位置,所以 a,b不在相等。a 原来指向的内存区域不会被回收,因为现在由 b 指向。可以看到 b 指向的内存地址和 a 之前的指向的内存地址是一致的。
当修改 mutable 对象时,由于都指向相同的内存地址,所以对变量 list_a 修改的操作,也会映射到变量 list_b。
总结一下:
指向 imutable 的不同变量,当其中一个变量被修改时,其他变量不受影响,因为被修改后的变量会指向一个新创建的对象。
指向 mutable 对象的不同变量,当其中一个变量修改这个对象时,会影响到指向这个对象的所有变量。
浅拷贝
浅拷贝创建了一个对象,这个对象包含了对被拷贝元素的参考。 所以当使用浅拷贝来复制 conainters
对象时,仅仅拷贝了那些嵌套元素的引用。
def shallow_copy():
# immutable objects
# int
a = 10
b = copy(a)
print('----- int')
print("id of a:{} , id of b: {}".format(id(a), id(b)))
# id of a:1777364320 , id of b: 1777364320
print(a == b) # True
print(a is b) # True
# str
str_a = '123'
str_b = copy(str_a)
print('----- str')
print("id of a:{} , id of b: {}".format(id(str_a), id(str_b)))
# id of a:2676110142128 , id of b: 2676110142128
print(str_a == str_b) # True
print(str_a is str_b) # True
# tuple
tuple_a = (1, 2, 3)
# Three methods of shallow copy
# tuple_b = tuple_a[:]
# tuple_b = tuple(tuple_a)
tuple_b = copy(tuple_a)
print(id(tuple_b))
print('----- tuple')
print("id of a:{} , id of b: {}".format(id(tuple_a), id(tuple_b)))
# id of a:2676110191640 , id of b: 2676110191640
print(tuple_a == tuple_b) # True
print(tuple_a is tuple_b) # True
# mutable
# set
set_a = {1, 2, 3}
# Two methods of shallow copy
# set_b = set(set_a)
set_b = copy(set_a)
print('----- set')
print("id of a:{} , id of b: {}".format(id(set_a), id(set_b)))
# id of a:2099885540520 , id of b: 2099888490984
print(set_a == set_b) # True
print(set_a is set_b) # False
# list
list_a = [1, 2, 3]
# Three methods of shallow copy
# list_b = list_a[:]
# list_b = list(list_b)
list_b = copy(list_a)
print('----- list')
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2099888478280 , id of b: 2099888478472
print(list_a == list_b) # True
print(list_a is list_b) # False
# dict
dict_a = {"name": "xxx", "age": "123"}
# Two methods of shallow copy
# dict_b = dict(dict_a)
dict_b = copy(dict_a)
print('----- dict')
print("id of a:{} , id of b: {}".format(id(dict_a), id(dict_b)))
# id of a:2099855880480 , id of b: 2099886881024
print(dict_a == dict_b) # True
print(dict_a is dict_b) # False
这里有一点需要注意,对于 string 和 number 来说,正如上面提到的 Cpython 做了相应的优化,让不同的变量指向了相同的内存地址,进而 id 的值是相等的。
但对于元组这个 immutable 元素来说,执行 浅拷贝时,也不会创建一个内存区域,只是返回一个老元组的引用。
对于其他的 mutable 对象,在浅拷贝后都会创建一个新的内存区域,包含了被拷贝元素的引用。
浅拷贝正如它的名字那样,当拷贝嵌套的 mutable 元素时,就会出现问题:
def shallow_copy_change_value():
# list
list_a = [1, 2, 3, [4, 5, 6]]
list_b = copy(list_a)
list_a[0] = 10
list_a[3].append(7)
print('----- list')
print("ia:{} ,b: {}".format(list_a, list_b))
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# a:[10, 2, 3, [4, 5, 6, 7]] ,b: [1, 2, 3, [4, 5, 6, 7]]
# id of a:1698595158472 , id of b: 1698595159752
print(list_a == list_b) # False
print(list_a is list_b) # False
下面是对上面 list 浅拷贝的图解:
执行浅拷贝操作:
在 list_b
执行浅拷贝后,创建一个新的对象,新对象中的 list_a[0]
指向 1.
修改 list_a
操作:
当执行 list_a[0] = 10
操作时,由于 list_a[0]
本身是 number 类型,会重新创建一块区域,用于保存新的值 10. 而新创建的 list_b[0]
并不会受到影响,还会指向之前的内存区域。
当修改list_a[3]
操作时,由于list_a[3]
在浅拷贝后,新创建的对象中不会 嵌套创建 一个新的 list_a[3]
对象,仅仅是指向了之前的 list_a[3]
对象。所以当修改 list_a[3]
时, list_b[3]
也会收到影响。
深拷贝
对于深拷贝操作来说,除了会创建一个新的对象外,会还递归的遍历老对象的中的嵌套元素,并形成新的副本。
def shallow_deepcopy_change_value():
# list
list_a = [1, 2, 3, [4, 5, 6]]
list_b = deepcopy(list_a)
list_a[0] = 10
list_a[3].append(7)
print('----- list')
print("a:{} ,b: {}".format(list_a, list_b))
print("id of a:{} , id of b: {}".format(id(list_a), id(list_b)))
# id of a:2099888478280 , id of b: 2099888478472
print(list_a == list_b) # False
print(list_a is list_b) # False
下面是对应图解过程:
执行深拷贝操作:
修改 list_a
操作:
这里 list_a
和 list_b
已经是完全的不同的两个对象。
总结
在这篇文章中,主要介绍了 Python 中对象,以及对象的拷贝过程,主要有下面几个重要的内容:
Python 中变量没有类型,仅仅可看做一个指针,通过引用指向对象。变量可以删除,但对象不行。
Python 对象被创建后,会拥有 identity,type 和 value 三个属性。
immutable 和 mutable,主要在于 value 在其生命周期内是否能发生变化。
修改 mutable 对象时,所有指向它的变量都会受到影响。修改 immutable 对象时,指向它的其他变量没有影响。
immutable 的大多数对象都是 hashable,但要考虑 immutable containers 的特殊情况。
浅拷贝会创建一个新的内存区域(对象),但其内部是对原对象内部引用的拷贝,在使用 mutable 对象时,存在一定的风险。
深拷贝不但会创建一个新的内存区域(对象),还会递归的创建原对象的所有嵌套对象,但也带来了一些效率的问题。
参考
Assignment statements in Python are more interesting than you might think
关于 Python 对象拷贝的那点事?的更多相关文章
- Python对象拷贝——深拷贝与浅拷贝
对象赋值 浅拷贝 深拷贝 1. 对象赋值 对象的赋值实际上是对对象的引用.也就是说当把一个对象赋值给另一个对象时,只是拷贝了引用.如: >>> t1 = tuple('furzoom ...
- Python 对象的引用计数和拷贝
Python 对象的引用计数和拷贝 Python是一种面向对象的语言,包括变量.函数.类.模块等等一切皆对象. 在python中,每个对象有以下三个属性: 1.id,每个对象都有一个唯一的身份标识自己 ...
- 《Python CookBook2》 第四章 Python技巧 对象拷贝 && 通过列表推导构建列表
(先学第四章) 对象拷贝 任务: Python通常只是使用指向原对象的引用,并不是真正的拷贝. 解决方案: >>> a = [1,2,3] >>> import c ...
- python中的对象拷贝
python中.进行函数參数传递或者返回值时,假设是一般的变量,会拷贝传递.假设是列表或字典则是引用传递.那python怎样对列表和字典进行拷贝传递呢:标准库的copy模块提供了两个方法:copy和d ...
- 【转】python中的对象拷贝
转自:https://www.cnblogs.com/bhlsheji/p/5352330.html python中.进行函数參数传递或者返回值时,假设是一般的变量,会拷贝传递.假设是列表或字典则是引 ...
- python中拷贝对象的区别
一.赋值.引用 在python中赋值语句总是建立对象的引用值,而不是复制对象.因此,python变量更像是指针,而不是数据存储区域 这点和大多数语音类似吧,比如C++.Java等 1.先看个例子: v ...
- Python 关于拷贝(copy)汇总(列表拷贝 // 字典拷贝 // 自定义对象拷贝)
1.列表拷贝 引用是指保存的值为对象的地址.在 Python 语言中,一个变量保存的值除了基本类型保存的是值外,其它都是引用,因此对于它们的使用就需要小心一些.下面举个例子: 问题描述:已知一个列表, ...
- Python比较操作符、变量赋值、对象拷贝
Python比较操作符.变量赋值.对象拷贝 目录 Python比较操作符.变量赋值.对象拷贝 1. 比较操作符 == 和 is 1.1 区别 1.2 实例 2. 变量及其赋值 2.1 概念和逻辑关系 ...
- Python开发【第二章】:Python深浅拷贝剖析
Python深浅拷贝剖析 Python中,对象的赋值,拷贝(深/浅拷贝)之间是有差异的,如果使用的时候不注意,就可能产生意外的结果. 下面本文就通过简单的例子介绍一下这些概念之间的差别. 一.对象赋值 ...
随机推荐
- 详解Java Web项目启动执行顺序
一. web.xml加载过程(步骤): 启动web项目,容器(如Tomcat.Apache)会去读取它的配置文件web.xml 中的两个节点,context-param和listener. 紧接着,容 ...
- java中常见的字符串API
java中定义了String类来封装字符串,并提供一系列的操作字符串的方法,他们都位于java.lang包下. package Main; public class String01 { public ...
- 算法<初级> - 第一章 排序相关问题
算法 - 第一章 时间复杂度: Big O 时间/空间复杂度计算一样,都是跟输入数据源的大小有关 n->∞ O(logn) 每次只使用数据源的一半,logn同理 最优解 先满足时间复杂度的情况最 ...
- The usage of Markdown---代码块
目录 1. 序言 2. 代码块 3. 引用中的代码 4. 列表中的代码块 更新时间:2019.09.14 1. 序言 在写技术博客的时候,我们常常需要添加一下代码块用来做演示说明,实际上在这篇博客 ...
- JavaScript基础修炼(14)——WebRTC在浏览器中如何获得指定格式的PCM数据
目录 一. PCM格式是什么 二. 浏览器中的音频采集处理 三. 需求实现 方案1--服务端FFmpeg实现编码 方案2--ScriptProcessorNode手动处理数据流 参考文献 示例代码托管 ...
- 论RSA算法的重要性 -RSA 简介
地球上最重要的算法 (这个说法似乎有点夸张了,但是当你了解了RSA算法后,就觉得不夸张了.) 如果没有 RSA 算法,现在的网络世界毫无安全可言,也不可能有现在的网上交易.上一篇文章 ssh 协议为什 ...
- 最强中文NLP预训练模型艾尼ERNIE官方揭秘【附视频】
“最近刚好在用ERNIE写毕业论文” “感觉还挺厉害的” “为什么叫ERNIE啊,这名字有什么深意吗?” “我想让艾尼帮我写作业” 看了上面火热的讨论,你一定很好奇“艾尼”.“ERNIE”到底是个啥? ...
- SpringBoot项目集成socketIo实现实时推送
netty-socketio maven依赖 <dependency> <groupId>com.corundumstudio.socketio</groupId> ...
- js中const,var,let区别
1.const定义的变量不可以修改,而且必须初始化(常量) const b = 5 // 正确 // const b // 错误,必须初始化 // b = 4 // 错误,不可被修改 console. ...
- iOS开发高级分享 - iOS上的设备标识符和指纹
苹果认可的标识符 Apple提供了各种API,以方便用户识别各种用途: 通用标识符(UDID) 在iOS的早期,苹果公司提供了一个uniqueIdentifier财产上UIDevice-亲切地称为ud ...