避坑Python深拷贝和浅拷贝

        近期在工作中遇到一个Bug,现象就是字典总是莫名其妙的被改掉了,可是走查代码也没有修改的地方。最后发现是另外一个变量在流程中改掉了,导致目标字典也被修改了,这就涉及到了Python中的深拷贝和浅拷贝。

目录

一:Python对象类型和标识

二:Python赋值的内存表现

1,右值是表达式

2,右值是已有变量

三:浅拷贝

四:深拷贝


一:Python对象类型和标识

        在C语言中,两个指针指向同一块,比较两个指针我们就知道他们指向的内存是一样的。那在Python中没有指针的概念,那怎么知道两个对象是指向同一块内存的呢。这就涉及到Python中对象的概念。

        我们都知道在 python 中一切皆为对象,每一个对象都会具有 identity,type 和 value 这三个内容。下面我们分别介绍这三个内容

1,Identity:我们可以理解为对象的身份证, 一旦对象被创建后,系统便会给对象打上一个标签就是Identity值,该值不会发生改变。可以理解成C语言中的内存地址。is 操作符,比较对象是否相等就是通过这个值。通过 id() 函数查看它的整数形式。

>>> a = 'ftz'
>>> b = 'csdn'
>>>
>>> id(a)
140280590921944
>>> id(b)
140280590922000
>>> a is b
False
>>> a = b
>>> id(a)
140280590922000
>>> id(b)
140280590922000
>>> a is b
True
>>>

2,Type:表示的是对象类型,类型决定行为,类型决定了可以支持的值和操作(如对列表来说,会有求长度的操作) ,另外和Identity 一样,在对象创建后,Type 也不会发生变化。通过 type() 函数可以得到对象的类型。

>>> testList = [1,2,3]
>>> type(testList)
<class 'list'>
>>>
>>> dir(testList)
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
>>>
>>> testDict = {'a':1}
>>>
>>> type(testDict)
<class 'dict'>
>>>
>>> dir(testDict)
['__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'clear', 'copy', 'fromkeys', 'get', 'items', 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
>>>

3,Value:用于表示的某些对象的值。当对象在创建后值可以改变称为 mutable,否则的话被称为 immutable,比如列表就是mutable,元组就是immutable

>>> testList = [1,2,3]
>>> testTuple = (1,3)
>>> testList[0] = 5
>>> testList
[5, 2, 3]
>>> testTuple[0]
1
>>> testTuple[0] = 5
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>>

二:Python赋值的内存表现

        为什么要在第二章介绍一下Python的赋值操作,Python的赋值并不像C语言中的那么单纯,这也是容易在不知觉的情况下入坑。

1,右值是表达式

先看看两个变量的右值相同,并且是整型和字符的情况

>>> a = 1
>>> b = 1
>>> id(a)
140280718452640
>>> id(b)
140280718452640
>>> a is b
True
>>>
>>> a = 'ftz'
>>> b = 'ftz'
>>> id(a)
140280590922000
>>> id(b)
140280590922000
>>> a is b
True
>>>

可以看到虽然a和b是分别赋值的,但是最后a和b都指向了同一个对象,那么是不是延伸到其他类型也是一样的呢

>>> a = [1,2,3]
>>> b = [1,2,3]
>>> id(a)
140280590913864
>>> id(b)
140280590902920
>>> a is b
False
>>>
>>> a = {'a':1}
>>> b = {'a':1}
>>> id(a)
140280719996248
>>> id(b)
140280590910952
>>> a is b
False
>>>
>>> a = (1,2)
>>> b = (1,2)
>>> id(a)
140280590914248
>>> id(b)
140280590915144
>>> a is b
False
>>>

可以看到对于常用的类型,列表,字典,元组,在分别赋值同一个值时,并不是指向同一个对象,那么为什么会有这个区别呢?

        在python中,对于 不变对象immutable 中的 number 和 string 来说,对其做了一定的优化,在创建相同的内容时,使其 指向了相同的内存地址,从而被复用。但是,Python 不会对所有 mutable 对象执行此操作,因为实现此功能需要一定的运行时成本。对于在内存中的对象来说,必须首先在内存中搜索对象(搜索意味着时间)。对于 number 和 string 来说,搜索到它们很容易,所以才对其做了这样的优化。

        对于其他类型的对象,虽然创建的内容相同,但都在内存中完全创建了一块新的区域。

2,右值是已有变量

>>> a = [1,2,3]
>>> b = a
>>> id(a)
140280590902920
>>> id(b)
140280590902920
>>> a is b
True
>>>
>>> a = 'ftz'
>>> b = a
>>> id(a)
140280590922000
>>> id(b)
140280590922000
>>> a is b
True
>>>

        当右边是已经存在的 Python 对象时,不论是什么类型的对象,都没有在内存中创建新的内容,仅仅是声明了一个新的变量指向之前内存中已经创建的对象。

        我们尝试去改变其中一个变量的值,看看另外一个变量的表现

>>> a = [1,2,3]
>>> b = a
>>>
>>> a[0] = 5
>>> a
[5, 2, 3]
>>> b
[5, 2, 3]
>>>
>>> a = 10
>>> b = a
>>> b
10
>>> a is b
True
>>> a = 11
>>> b
10
>>> a is b
False

        修改 imutable 对象时,如上面的a = 11操作,由于其本身不可改变,只能在内存中新申请一块新的空间,用于存储修改后的内容。

        当修改 mutable 对象时,如上面的a = [1,2,3]操作,由于都指向相同的内存地址,所以对变量 a修改的操作,也会映射到变量b。

但是当其中一个重新赋值时,会是什么现象

>>> a = [1,2,3]
>>> b = a
>>> a is b
True
>>> a = [4,5]
>>> a is b
False
>>>

当重新对a赋值后,a和b没有指向同一个对象了,b继续指向a赋值前的对象,a指向新的对象

三:浅拷贝

        浅拷贝,指的是创建一个新的对象,但里面的元素是原对象中各个子对象的引用。对于浅拷贝有以下三点:

  • 使用数据类型本身的构造器
  • 对于可变的序列,还可以通过切片操作符 : 来完成浅拷贝
  • Python 还提供了对应的函数 copy.copy() 函数,适用于任何数据类型

我们用copy.copy进行浅拷贝时,可变对象和不可变对象的行为是不一样的

对于不可变对象immutable 

>>> from copy import copy
>>> a = 1
>>> b = copy(a)
>>> id(a)
140280718452640
>>> id(b)
140280718452640
>>> a is b
True
>>> a == b
True
>>>
>>> str_a = 'ftz'
>>> str_b = copy(str_a)
>>> id(str_a)
140280590922056
>>> id(str_b)
140280590922056
>>> str_a is str_b
True
>>> str_a == str_b
True
>>>
>>> tuple_a = (1,2,3)
>>> tuple_b = copy(tuple_a)
>>> id(tuple_a)
140280590911816
>>> id(tuple_b)
140280590911816
>>> tuple_a is tuple_b
True
>>> tuple_a == tuple_b
True
>>>

        对于不可变对象immutable 来说,Python 做了相应的优化,让不同的变量指向了相同的内存地址,进而 id 的值是相等的

对于可变对象mutable ,行为也是和上面一样吗?

>>> list_a = [1,2,3]
>>> list_b = copy(list_a)
>>> id(list_a)
140280590913928
>>> id(list_b)
140280590914568
>>> list_a is list_b
False
>>> list_a == list_b
True
>>>
>>> dict_a = {'a':1}
>>> dict_b = copy(dict_a)
>>> id(dict_a)
140280590910952
>>> id(dict_b)
140280719996248
>>> dict_a is dict_b
False
>>> dict_a == dict_b
True
>>>

        可以看到对于可变对象,行为和不可变对象是不一样的,浅拷贝的时候是重新分配了一块内存,包含了被拷贝元素的引用。

        既然可变对象和不可变对象的浅拷贝行为不一样,那假如一个结构里既有可变对象又有不可变对象,进行浅拷贝对发生什么现象

>>> list_a = [1,2,3,{'a':1}]
>>> list_b = copy(list_a)
>>>
>>> list_a[0] = 10
>>> list_b
[1, 2, 3, {'a': 1}]
>>>
>>> list_a[3]['a'] = 5
>>> list_b
[1, 2, 3, {'a': 5}]
>>>

        当执行 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] 也会收到影响。

四:深拷贝

        深拷贝,是指重新分配一块内存,创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。因此,新对象和原对象没有任何关联。

        在Python 中以 copy.deepcopy() 来实现对象的深度拷贝,我们还是以上面的最后一个例子来看看深拷贝的行为

>>> from copy import deepcopy
>>> list_a = [1,2,3,{'a':1}]
>>> list_b = deepcopy(list_a)
>>> list_b
[1, 2, 3, {'a': 1}]
>>> list_a[3]['a'] = 5
>>> list_b
[1, 2, 3, {'a': 1}]
>>>

        可以看到此时list_a的任何操作不会影响到list_b

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ftzchina

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

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

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

打赏作者

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

抵扣说明:

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

余额充值