“回👋掏”。最近做东西,有点儿玩不转复杂数据类型,写篇博文再回顾下深、浅拷贝相关知识。深、浅的区分主要在对复杂数据类型进行操作的时候。
By the way:时间过得很快,十月了,之前定了个小目标:一个月至少一篇文章产出
。2020年的 $ \frac{5}{6} $ 已经过去。很庆幸自己坚持了下来,学到了不少东西。实习期间其实有不少的文章主题的想法,但真正想动手写篇博文的时候,发现事情并没有想想中的那么简单,一个主题涉及到的知识点还是蛮多的,再加上实践经验的不足,有些东西很难写道点上,copy & paste 总是不太好的『努力提高文章质量,hhh~』。希望自己后续继续加油。
浅拷贝(shallow copy)
浅拷贝总结:新对象内容为原对象内第一层对象的引用 。
Python 中的浅拷贝 关键点就在于这第一层对象。让我们先看看 Python 中的浅拷贝。
先看看不含嵌套元素的情形:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 l1 = [1 , 2 , 3 ] l2 = l1 print (l1 is l2) l3 = list (l1) print (l1 is l3) l4 = l1[:] print (l1 is l4) print (id (l1), id (l2), id (l3), id (l4))
含嵌套元素的情形:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 l1 = [1 , [2 ,3 ], 4 ] l2 = l1 l3 = list (l1) l4 = l1[:] for first, second, third, fourth in zip (l1, l2, l3, l4): print ("value" , first, "address:" , id (first), id (second), id (third), id (fourth)) l4[1 ].append("new" ) print (l1) print (l2) print (l3) print (l4) for first, second, third, fourth in zip (l1, l2, l3, l4): print ("value" , first, "address:" , id (first), id (second), id (third), id (fourth))
从上面的示例可以看到,Python中切片操作、工厂函数和=
操作均是浅拷贝,只拷贝了原对象的第一层对象的引用,对第一层对象的操作会影响到其它对元对象进行浅拷贝的对象。但=
操作和切片、构造器(工厂函数)不同的是,=
操作不会创建新的对象。
值得注意的是,Python 中 tuple 的 tuple() 和切片操作和=
进行的拷贝一样,不会创建新的对象。字典的浅拷贝可以使用 dict.copy()。
JS 中的浅拷贝 让我们再来看看 JS 中的浅拷贝操作。
老规矩,先看看简单对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let obj1 = { a : 1 , b : 2 }; let obj2 = obj1; let obj3 = Object .assign ({}, obj1); console .log (obj3)let obj4 = {...obj1}; obj2.a = "new" ; console .log (obj1, obj2, obj3, obj4)
再看下复杂对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let obj1 = { a : { b : 1 , c : 2 }, d : 3 }; let obj2 = obj1; let obj3 = Object .assign ({}, obj1); let obj4 = {...obj1}; obj2.a .b = "new" ; console .log (obj1); console .log (obj2); console .log (obj3); console .log (obj4);
可以看到,JS 对象的=
操作、Object.assign({}, originObject) 和对象扩展运算均是浅拷贝。但是 Object.assign和对象的扩展运算对只有一层的对象进行的是深拷贝。此外 JS 数组「array 也是 object」的 map、reduce、filter、slice 等方法对嵌套数组进行的也是浅拷贝操作。
可以明显的看到,JS 和 Python 中的浅拷贝拷贝的均是第一层对象的引用。
深拷贝(deep copy)
深拷贝总结:创建一个新的对象,并且将原对象中的元素,以递归的方式,通过创建新的子对象拷贝到新对象中。深拷贝拷贝了对象的所有元素,包括多层嵌套的元素。
Python 中的深拷贝 在 Python 中实现复杂对象的拷贝可以通过标准库copy 提供的 copy.deepcopy 实现,此外 copy 模块还提供了 copy.copy 进行对象的浅拷贝。
看下深拷贝的情况:
1 2 3 4 5 6 7 8 import copyl1 = [1 , [2 , 3 ], 4 ] l2 = copy.deepcopy(l1) l2[1 ].append("new" ) print (l1) print (l2)
可以看到,有别于浅拷贝,对深拷贝 l1 的新对象 l2 的子元素增加新元素,并不会影响到 l1。
JS 中的深拷贝 在 JS 中进行复杂对象的深拷贝,可以使用 JSON.stringify 先将 JS 对象转成 JSON 再转 JS 对象,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let obj1 = { a : { b : 1 , c : 2 }, d : 3 }; let obj2 = JSON .parse (JSON .stringify (obj1));obj2.a .b = "new" ; console .log (obj1); console .log (obj2);
可以看到,深拷贝后对新对象深层次对象的更改不会使原对象发生变更。
手动实现深拷贝操作 在某些情况下需要我们实现深拷贝操作,比如对自定义数据类型进行深拷贝。前面 JS 所述使用 JSON 进行的深拷贝方法仍有缺陷,比如:会忽略 undefined、会忽略 symbol、不能序列化函数、不能解决循环引用的对象。这时候就需要了解波深拷贝的实现了。
从前面所述可知,深拷贝与浅拷贝的区别主要在于 copy 的层次,浅拷贝 copy 的是第一层对象的引用,深拷贝需要 copy 深层次对象。So,以 deepcopy 层次 Object 为例子,要实现真正的深拷贝操作则需要通过遍历键来赋值对应的值,这个过程中如果遇到 Object 类型还需要再次进行遍历「同样的方法」。递归无疑了。来看波实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 function deepclone (obj ) { let map = new WeakMap (); function deep (data ) { let result = {}; const keys = [...Object .getOwnPropertyNames (data), ...Object .getOwnPropertySymbols (data)] if (!keys.length ) return data; const exist = map.get (data); if (exist) return exist; map.set (data, result); keys.forEach (key => { let item = data[key]; if (typeof item === 'object' && item) { result[key] = deep (item); } else { result[key] = item; } }) return result; } return deep (obj); }
OK,再看些 Python 的 copy.deepcopy 的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 def deepcopy (x, memo=None , _nil=[] ): """Deep copy operation on arbitrary Python objects. See the module's __doc__ string for more info. """ if memo is None : memo = {} d = id (x) y = memo.get(d, _nil) if y is not _nil: return y ...
emm…,实现思想也是使用递归,同时借助了 memo (备忘录)解决对象的循环引用问题,避免 StackOverflow。
参考