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