Python3 可变对象VS不可变对象、 对象的赋值、深拷贝VS浅拷贝

  1. 可变对象:当有需要改变对象内存的值的时候,这个对象的id不发生变化。

  2. 不可变对象:当有需要改变对象内存的值的时候,这个对象的id会发生变化。也就是不同的值指向不同的内存地址,也就是该变量的内存值不可改变。

这里的变与不变是针对同一个内存地址的。可变是指该对象所指定的内存地址上面的值可以被改变,变量被改变后,其所指向的内存地址上面的值,直接被改变,没有发生复制行为,也没有发生开辟新的内存地址行为。

Python3中,有6个标准的数据类型,他们又分为可变和不可变。

不可变数据(3个):

  • Number(数字)
  • String(字符串)
  • Tuple(元组)

可变数据(3个):

  • List(列表)
  • Dictionary(字典)
  • Set(集合)
PS C:\Users\lenovo> python
Python 3.7.3 | packaged by conda-forge | (default, Mar 27 2019, 23:18:50) [MSC v.1900 64 bit (AMD64)] :: Anaconda, Inc. on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> a_list = [1,2,3,4,5,6,7]
>>> id(a_list)
2525246612104
>>> a_list = [1,2,3,4,5,6,7,8]
>>> id(a_list)
2525276250760
>>> a_list = [1,2,3,4,5,6,7]
>>> id(a_list)
2525246612104
>>> b_list = [1,2,3,4,5,6,7]
>>> id(b_list)
2525276250760
>>> a=7
>>> b=7
>>> id(a)
140736117558816
>>> id(b)
140736117558816
>>> a=8
>>> id(a)
140736117558848
>>> a=9
>>> id(a)
140736117558880
a = 65888
b = 65888
id(a)
Out[5]: 2541828276304
id(b)
Out[6]: 2541828276368
>>> c = set([1,2,3])
>>> id(c)
2525278645608
>>> c.add(5)
>>> id(c)
2525278645608
str_a = "string"
str_b = "string"
id(str_a)
Out[9]: 2540998727248
id(str_b)
Out[10]: 2540998727248
str_b = "string41654scf5safdskjfafd"
str_a = "string41654scf5safdskjfafd"
id(str_b)
Out[13]: 2541830458144
id(str_a)
Out[14]: 2541830458144

由这些现象说四个内部实现:

int 类型解析

较小的整数会很频繁的被使用,所以python将这些对象放置到了一个池子中,每次需要这些对象的时候就到池子中获取这个值,避免多次的重复创建对象引起的许多不必要的开销。这个池子内的数字范围是[-5, 257), 所以都是从池子里面取值,自然id不变。

float类型解析

对于float类型的使用自然没有int那么频繁,并且float类型也不好定义哪些常用,也就没有池子给到这个类型,所以每次重新创建即可。

tuple类型解析

对于tuple类型,与float类型的思维相似,所以也是每次重新创建。

string类型解析

单词类型的str由于被重复使用的概率比较大,所以在python中为单词类型的str做了一个缓存,也就是说如果是单词类型的str, 会被存储到一个字典(dict)中,字典的内容是字符串为key, 地址为value。当有一个字符串需要创建,就先去访问这个字典,如果存在则返回字典中字符串的地址,如果不存在,则返回新创建的地址,并将这个字符串添加进入字典。这是字符串的intern机制。

 

 

对象赋值:内存地址之间的传递,值不变,内存空间一般不变,值改变内存地址改变。

>>> will = ["Will", 28, ["Python", "C#", "JavaScript"]]
>>> wilber = will
>>> id(will)
2525278694920
>>> will
['Will', 28, ['Python', 'C#', 'JavaScript']]
>>> [id(ele) for ele in will]
[2525278632176, 140736117559488, 2525278628168]
>>> id(wilber)
2525278694920
>>> wilber
['Will', 28, ['Python', 'C#', 'JavaScript']]
>>> [id(ele) for ele in wilber]
[2525278632176, 140736117559488, 2525278628168]
>>>
>>> will[0] = "Wilber"
>>> will[2].append("CSS")
>>> id(will)
2525278694920
>>> will
['Wilber', 28, ['Python', 'C#', 'JavaScript', 'CSS']]
>>> [id(ele) for ele in will]
[2525278686992, 140736117559488, 2525278628168]
>>> id(wilber)
2525278694920
>>> wilber
['Wilber', 28, ['Python', 'C#', 'JavaScript', 'CSS']]
>>> [id(ele) for ele in wilber]
[2525278686992, 140736117559488, 2525278628168]

下面来分析一下这段代码:

  • 首先,创建了一个名为will的变量,这个变量指向一个list对象,可以看到所有对象的地址(每次运行,结果可能不同)
  • 然后,通过will变量对wilber变量进行赋值,那么wilber变量将指向will变量对应的对象(内存地址),也就是说"wilber is will","wilber[i] is will[i]"

    • 可以理解为,Python中,对象的赋值都是进行对象引用(内存地址)传递
  • 第三张图中,由于will和wilber指向同一个对象,所以对will的任何修改都会体现在wilber上

    • 这里需要注意的一点是,str是不可变类型,所以当修改的时候会替换旧的对象,产生一个新的地址2525278686992

 

浅拷贝和深度拷贝

浅拷贝

copy模块里面的copy方法实现

  • 1、对于 不可变 类型 Number、String、Tuple,浅复制仅仅是地址指向,不会开辟新空间。
  • 2、对于 可变类型 List、Dictionary、Set,浅复制会开辟新的空间地址(仅仅是最顶层开辟了新的空间,里层的元素地址还是一样的),进行浅拷贝。
  • 3、浅拷贝后,改变原始对象中为可变类型的元素的值,会同时影响原始拷贝对象的;改变原始对象中为不可变类型的元素的值,只有原始类型受影响(操作拷贝对象对原始对象的也是同理)

 

>>> import copy
>>>
>>> will = ["Will", 28, ["Python", "C#", "JavaScript"]]
>>> wilber = copy.copy(will)
>>>
>>> id(will)
2041111024392
>>> will
['Will', 28, ['Python', 'C#', 'JavaScript']]
>>> [id(ele) for ele in will]
[2041111133352, 140736117559488, 2041111125512]
>>> id(wilber)
2041111178376
>>> wilber
['Will', 28, ['Python', 'C#', 'JavaScript']]
>>> [id(ele) for ele in wilber]
[2041111133352, 140736117559488, 2041111125512]
>>>
>>> will[0] = "Wilber"
>>> will[2].append("CSS")
>>> id(will)
2041111024392
>>> will
['Wilber', 28, ['Python', 'C#', 'JavaScript', 'CSS']]
>>> [id(ele) for ele in will]
[2041111116408, 140736117559488, 2041111125512]
>>> id(wilber)
2041111178376
>>> wilber
['Will', 28, ['Python', 'C#', 'JavaScript', 'CSS']]
>>> [id(ele) for ele in wilber]
[2041111133352, 140736117559488, 2041111125512]

分析一下这段代码:

  • 首先,依然使用一个will变量,指向一个list类型的对象
  • 然后,通过copy模块里面的浅拷贝函数copy(),对will指向的对象进行浅拷贝,然后浅拷贝生成的新对象赋值给wilber变量

    • 浅拷贝会创建一个新的对象,这个例子中"wilber is not will"
    • 但是,对于对象中的元素,浅拷贝就只会使用原始元素的引用(内存地址),也就是说"wilber[i] is will[i]"
  • 当对will进行修改的时候

    • 由于list的第一个元素是不可变类型,所以will对应的list的第一个元素会使用一个新的对象2041111116408
    • 但是list的第三个元素是一个可不类型,修改操作不会产生新的对象,所以will的修改结果会相应的反应到wilber上

 

总结一下,当我们使用下面的操作的时候,会产生浅拷贝的效果:

  • 使用切片[:]操作
  • 使用工厂函数(如list/dir/set)
  • 使用copy模块中的copy()函数

深拷贝

copy模块里面的deepcopy方法实现

  • 1、浅拷贝,除了顶层拷贝,还对子元素也进行了拷贝(本质上递归浅拷贝)
  • 2、经过深拷贝后,原始对象和拷贝对象所有的元素地址都没有相同的了

>>> import copy
>>> will = ["Will", 28, ["Python", "C#", "JavaScript"]]
>>> wilber = copy.deepcopy(will)
>>> id(will)
2041111178248
>>> will
['Will', 28, ['Python', 'C#', 'JavaScript']]
>>> [id(ele) for ele in will]
[2041111133352, 140736117559488, 2041111177416]
>>> id(wilber)
2041111024392
>>> wilber
['Will', 28, ['Python', 'C#', 'JavaScript']]
>>> [id(ele) for ele in wilber]
[2041111133352, 140736117559488, 2041111178568]
>>> will[0] = "Wilber"
>>> will[2].append("CSS")
>>> id(will)
2041111178248
>>> will
['Wilber', 28, ['Python', 'C#', 'JavaScript', 'CSS']]
>>> [id(ele) for ele in will]
[2041111116352, 140736117559488, 2041111177416]
>>> id(wilber)
2041111024392
>>> wilber
['Will', 28, ['Python', 'C#', 'JavaScript']]
>>> [id(ele) for ele in wilber]
[2041111133352, 140736117559488, 2041111178568]

 

 

分析一下这段代码:

  • 首先,同样使用一个will变量,指向一个list类型的对象
  • 然后,通过copy模块里面的深拷贝函数deepcopy(),对will指向的对象进行深拷贝,然后深拷贝生成的新对象赋值给wilber变量

    • 跟浅拷贝类似,深拷贝也会创建一个新的对象,这个例子中"wilber is not will"
    • 但是,对于对象中的元素,深拷贝都会重新生成一份(有特殊情况,下面会说明),而不是简单的使用原始元素的引用(内存地址)

      • 例子中will的第三个元素指向39737304,而wilber的第三个元素是一个全新的对象,也就是说,"wilber[2] is not will[2]"
  • 当对will进行修改的时候

    • 由于list的第一个元素是不可变类型,所以will对应的list的第一个元素会使用一个新的对象;
    • 但是list的第三个元素是一个可不类型,修改操作不会产生新的对象,但是由于"wilber[2] is not will[2]",所以will的修改不会影响wilber

拷贝的特殊情况

其实,对于拷贝有一些特殊情况:

  • 对于非容器类型(如数字、字符串、和其他'原子'类型的对象)没有拷贝这一说

    • 也就是说,对于这些类型,"obj is copy.copy(obj)" 、"obj is copy.deepcopy(obj)"
  • 如果元祖变量只包含原子类型对象,则不能深拷贝,看下面的例子
tuple_a = (1,2,3) 
tuple_b = copy.deepcopy(tuple_a)
id(tuple_a)
Out[24]: 2541828267536
id(tuple_b)
Out[25]: 2541828267536

总结

本文介绍了对象的赋值和拷贝,以及它们之间的差异:

  • Python中对象的赋值都是进行对象引用(内存地址)传递
  • 使用copy.copy(),可以进行对象的浅拷贝,它复制了对象,但对于对象中的元素,依然使用原始的引用.
  • 如果需要复制一个容器对象,以及它里面的所有元素(包含元素的子元素),可以使用copy.deepcopy()进行深拷贝
  • 对于非容器类型(如数字、字符串、和其他'原子'类型的对象)没有被拷贝一说
  • 如果元祖变量只包含原子类型对象,则不能深拷贝。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ljtyxl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值