Python copy模块浅拷贝和深拷贝
在开发中,经常涉及到数据的传递,在数据传递使用的过程中,可能会对数据进行修改。
对数据进行处理后,如果在后面的代码中,即需要使用修改之前的数据,也需要使用修改之后的数据,就要在修改前对数据进行拷贝。
拷贝数据后,有两份数据,只修改其中一份数据,不修改另一份数据,到最后依然能保留修改前的数据。
一、Python 实现数据拷贝的方法
运行结果:
上面的代码使用了四种方式来对数据进行拷贝,这些方法都可以用来拷贝数据,结果都一样。
1.切片
对需要拷贝的数据进行切片处理,返回的结果相当于拷贝了一份数据。
2.工厂方法
使用 python 的工厂函数 list 来拷贝数据。(python的工厂函数是比较特殊的,即是类也是函数,关于工厂函数的理解可以另行扩展一下)
拷贝列表时使用 list,如果拷贝字符串则将上面的 list 换成 str ,以此类推。
3.list对象的copy方法
python 中的 list 实现了 copy 方法,在拷贝列表时可以直接使用。这里需要注意,比如 str 没有实现 copy 方法,拷贝字符串时使用其他方法拷贝。
4.copy模块的copy方法
在 Python 标准库中有一个 copy 模块,可以使用 copy 模块的 copy() 方法来拷贝数据,copy 模块可以拷贝所有类型的数据。
二、拷贝的数据被修改
运行结果:
在实际工作中,数据的嵌套层数是很多的,通常会嵌套好几层。上面就在 base 列表中嵌套了一个 son 子列表。
用上面的四种拷贝方法拷贝 base 列表,然后修改 base 列表中的子列表 son 。重新打印这几个列表,发现不仅 base 列表被修改了,拷贝的列表也全部被修改了。
现在的需求是拷贝一份数据,修改一份保留一份,如果两份数据都被修改,是不符合需求的。
上面的四种拷贝方法都被称为浅拷贝(相对深拷贝而言),浅拷贝 Python 中的可变对象,如果数据中嵌套了可变对象,修改嵌套的可变对象,所有拷贝的数据都会一起被修改。
三、Python 可变对象和不可变对象
在 Python 中,所有的数据都是对象,无论是数字,字符串,元组,列表,字典,还是函数,类,甚至是模块。
不可变对象:
int, str, tuple 等类型的数据是不可变对象,不可变对象的特性是数据不可被修改。
运行结果:
如果对不可变对象修改,其实不是修改变量对象,而是重新创建一个同名的变量对象。可以通过 id 函数来判断,id 不一样就证明已经不是同一个变量了。
可变对象:
list, set,dict 等类型的数据是可变对象,相对于不可变对象而言,可变对象的数据可以被修改,修改之后还是同一个id。
运行结果:
对可变对象进行修改,修改后还是同一个对象,只是可变对象里面的元素指向了不同的数据,这种指向是通过引用的方式来实现的。
上面的代码是对列表进行修改,如果对元组这样修改,代码会报错,就是因为可变对象和不可变对象的区别。
四、Python 中的引用和引用传递
在 Python 程序中,每个对象都会在内存中开辟一块空间来保存该对象,该对象在内存中所在位置的地址被称为引用。
在编写代码时,定义的变量名实际是定义指向对象的地址引用名。
我们定义一个列表时,变量名是列表的名字,这个名字指向内存中的一块空间。这个列表里有多个元素,表示这块内存空间中,保存着多个元素的引用。
1. 修改引用
当修改列表的元素时,其实是修改列表中的引用。
运行结果:
例如,修改 list_a 中的第三个元素,其实是修改第三个元素的引用(这块内存指向的对象)。如下图:
2. 引用传递(拷贝)
当拷贝列表时,其实是拷贝列表中的引用。
运行结果:
例如,拷贝 list_b 到 list_c,其实是给 list_c 新开辟一块内存,然后拷贝一份 list_b 的引用给 list_c ,并不是将 list_b 指向的对象拷贝一份。如下图:
注意:这里不是将 list_b 赋值给 list_c,那样的结果是 list_b 指向 [1, 2, 3] ,list_c 指向 list_b,是引用关系,而不是拷贝关系。上面列举拷贝的方法时,没有将赋值列为拷贝方法,因为赋值是引用的传递,而不是拷贝。
五、浅拷贝时数据被修改
1. 拷贝后修改引用(数据无嵌套)
运行结果:
使用 copy.copy() 方法拷贝 list_b 到 list_c,然后修改 list_b 中的引用关系,这样, list_c 不会被修改。如下图:
2.嵌套列表的拷贝
运行结果:
对于嵌套的列表,拷贝 list_d 到 list_e,也是拷贝一份 list_d 的引用给 list_e ,与不嵌套的相同。
这里需要特别注意,在浅拷贝嵌套的列表时,只会拷贝最上层的引用,对于子列表的引用,不会拷贝。如下图:
3.拷贝的列表随原列表一起被修改
运行结果:
拷贝 list_d 到 list_e,由于没有拷贝子列表的引用 ,当修改子列时, list_d 和 list_e 都引用了子列表 sub,所以 list_d 和 list_e都会被修改。如下图:
拷贝数据后,修改其中一个,另一个也跟着被修改,原因就是浅拷贝中,只拷贝了最外层的引用。当修改内层的引用时,所有外层的引用不变,都会指向修改后的结果。
两份数据都被修改,这就是浅拷贝中存在的问题,需要使用深拷贝来解决。
六、深拷贝保证数据不会被修改
运行结果:
使用 copy 模块的 deepcopy() 方法,在拷贝数据时,会递归地拷贝数据中所有嵌套的引用。
使用 deepcopy() 拷贝 list_d 到 list_f ,然后修改 list_d 中子列表的引用,不会对 list_f 产生影响,所以 list_f 不会被修改。如下图:(可以和上面的图进行对比)
这样,就可以达到复制数据,一份修改,一份不修改的目的。
在工作中,这种情况不是特别多,所以出现的时候很容易掉坑。比如说,有一个复杂的列表(字典、列表多层嵌套),第一次获取数据写入数据库,后面每次获取数据都要与上一次的数据对比去重,然后把本次获取的数据覆盖数据库中的数据。这就是获取数据,修改数据,最后还需要使用修改前的数据。这时候用浅拷贝,极易出错,对于较大的数据(如一个1M大的json数据),出错了还不易发现错误。
为了解决和避免这种错误,可以使用深拷贝 deepcopy()。
在Python中,浅拷贝消耗的内存和运行效率都优于深拷贝,所以默认的拷贝都是浅拷贝。
对可能需要使用深拷贝的情况,要特别留意,使用深拷贝,避免出错。