Python 深拷贝 vs 浅拷贝

在一些业务场景中, 有时候我们需要复制一个对象, 但是又不想对原来的对象产生影响, 就想搞个 副本 来为所欲为地操作嘛. 但是呢, 在 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中变量的本质是指针, 而万物皆对象的对象分为, 可变和不可变 (能不能 原地修改 还是需要新变量接收)
  • 问题都是出在 整副本 的过程, 副本就是深拷贝, 完全复一个新的对象, 地址也不同了, 跟原配彻底分手
  • 不可变对象 不存在深浅拷贝一说, 是唯一的, 只有引用. 像, 字符串, 数字, 元组.
  • 浅拷贝, 虽然外层地址变了, 换了个对象, 但里面的元素, 还是原来的引用, 还是藕断丝连的哦, 即里面的元素如果是可变类型的, 一个改变了, 另外的也会受影响的哦.
  • 列表的切片, 是 浅拷贝. 也是之前被坑过. 还以为是找了个新对象, 没想到, 是我太天真了...

原文地址:https://www.cnblogs.com/chenjieyouge/p/12254099.html

时间: 2024-10-19 12:22:21

Python 深拷贝 vs 浅拷贝的相关文章

完全理解python深拷贝和浅拷贝

import copya = [1, 2, 3, 4, ['a', 'b']]  #原始对象b = a  #赋值,传对象的引用c = copy.copy(a)  #对象拷贝,浅拷贝d = copy.deepcopy(a)  #对象拷贝,深拷贝a.append(5)  #修改对象aa[4].append('c')  #修改对象a中的['a', 'b']数组对象print 'a = ', aprint 'b = ', bprint 'c = ', cprint 'd = ', d 输出结果:a = 

python深拷贝和浅拷贝之简单分析

title: python 深拷贝和浅拷贝 tags: python,copy,deepcopy grammar_cjkRuby: true --- python 深拷贝和浅拷贝 python的变量的赋值都是引用 把一个变量赋值给一个变量,不是拷贝这个对象,而是拷贝这个变量的引用 直接赋值 传递的是这个变量的引用 浅拷贝 拷贝的是这个变量的引用,会产生新的对象 浅拷贝会产生一个新的对象,但是浅拷贝的内容还是原有对象的引用 看下面的例子 浅拷贝 import copy a = [1, 2, 3,

python 深拷贝和浅拷贝之可变和不可变对象总结

了解深拷贝和浅拷贝之前先要理解可变与不可变对象 python只允许使用引用传递,有可变对象和不可变对象,可变对象:list,dict.不可变对象有:int,string,float,tuple Python int,string,float,tuple不可变举栗子: def int_object(): i = 89 j = 89 print(id(89)) print('i id:' + str(id(i))) print('j id:' + str(id(j))) print(i is j)

python 深拷贝与浅拷贝

浅拷贝的方式有: lst=[1,2,3] (1)直接赋值: lst_cp = lst (2)for循环遍历生成:lst_cp= [i for i in lst] (3)copy模块下,copy.copy仍为浅拷贝 深拷贝的方式 (1)借助copy模块 >>> import copy >>> lst_cp = copy.deepcopy(lst) 以上方法的测试: 注意:因为string类型是不可变类型,所以修改string元素时会新创建一个地址空间放置数据 (1)直接赋

Python深拷贝和浅拷贝

1. Python引用计数[ http://blog.chinaunix.net/uid-26602509-id-3506965.html ] 1.1 引用计数机制 引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象.内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程.使用引用计数技术可以实现自动资源管理的目的.同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法. 当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象

Python 深拷贝和浅拷贝的区别

python的复制,深拷贝和浅拷贝的区别    在python中,对象赋值实际上是对象的引用.当创建一个对象,然后把它赋给另一个变量的时候,python并没有拷贝这个对象,而只是拷贝了这个对象的引用 一般有三种方法,        alist=[1,2,3,["a","b"]] (1)直接赋值,传递对象的引用而已,原始列表改变,被赋值的b也会做相同的改变 >>> b=alist        >>> print b        

python深拷贝与浅拷贝

浅拷贝:b=copy.copy(a),对引用的拷贝,只拷贝父对象,所以id(b)不等于id(a),但对象内部资源依然引用,内部id(b[0])等于id(a[0]),id(b[4])等于id(a[4]) 深拷贝:c=copy.deepcopy(a),对对象资源的拷贝,id(c)不等于id(a),内部id(c[0])等于id(a[0]),id(c[4])不等于id(a[4]) 注意:因为数字是不可变量,所以数字的id不变,无论深拷贝还是浅拷贝,id(b[0]).id(c[0])都等于id(a[0])

python深拷贝和浅拷贝的区别

首先深拷贝和浅拷贝都是对象的拷贝,都会生成一个看起来相同的对象,他们本质的区别是拷贝出来的对象的地址是否和原对象一样,也就是地址的复制还是值的复制的区别. 深拷贝和浅拷贝需要注意的地方是可变元素的拷贝,在浅拷贝时:拷贝出来的新对象的地址和原对象是不一样的,但是新对象里面的可变元素(如列表)的地址和原对象里的可变元素的地址是相同的,也就是说浅拷贝它拷贝的是浅层次的数据结构(不可变元素),对象里的可变元素作为深层次的数据结构并没有被拷贝到新地址里面去,而是和原对象里的可变元素指向同一个地址,所以在新

Python 深拷贝和浅拷贝

Python中,对象的赋值,拷贝(深/浅拷贝)之间是有差异的,如果使用的时候不注意,就可能产生意外的结果. 下面本文就通过简单的例子介绍一下这些概念之间的差别. 对象赋值 直接看一段代码: will=["Will",28,["Python","C#","JavaScript"]] wilber=will print id(will) print will print [id(x) for x in will] print id

图解Python深拷贝和浅拷贝

Python中,对象的赋值,拷贝(深/浅拷贝)之间是有差异的,如果使用的时候不注意,就可能产生意外的结果. 下面本文就通过简单的例子介绍一下这些概念之间的差别. 对象赋值 直接看一段代码: will = ["Will", 28, ["Python", "C#", "JavaScript"]] wilber = will print id(will) print will print [id(ele) for ele in wi