前言
昨天刷公众号看到一篇描述py优雅语法的文章,心痒之下到家就开始尝试,学习了for else statement,yield和py版三目写法。在列表切片这部分中,对作者的列表拷贝写法,有些不太理解。
# 拷贝 copy_items = items[::] 或者 items[:]
尝试
首先开一个python,随便建一个列表l=[1,2,3]
将其进行两种方法的拷贝:
- 我的写法
c=l
- 作者的写法
d=l[:]
分别打印了c
和d
,并没有什么差别,仔细斟酌了一下作者的用意,觉得应该有一些深层次的考虑。
于是使用id()
分别查看两边的内存地址,这一打印出现了不同的结果。
>>> id(l) 39179656L >>> id(c) 39179656L >>> id(d) 39179272L
使用直接赋值的c
的内存地址和原列表l
的内存地址是一样的,而用切片方法拷贝的d
的内存地址不一样。
我尝试改动l
的值看一下结果。
>>> l.append('z') >>> l [1, 2, 3, 'z'] >>> c [1, 2, 3, 'z'] >>> d [1, 2, 3]
此时区别就显示出来了,使用直接赋值的c
因为和原列表l
指向同一个内存地址,所以当修改l
的值的时候,打印c
也发现同样的改变。
探索
稍微搜一下怎么拷贝一个列表,发现这个是有专有名词的,叫做 “深浅拷贝” (copy,deepcopy)。我原来直接赋值的写法只是将内存地址的引用传递到一个新的对象里,连浅拷贝都算不上。
Python的拷贝有一个专门的模块,叫做copy
。
浅拷贝
import copy; >>> l=[1,2,3,[4,5],6] >>> c=copy.copy(l) >>> id(l) 39195912L >>> id(c) 39238600L
从内存引用里清晰的显示,至少内存地址不一样了,对l
进行内容变更应该不会影响到c
。
>>> l.append('z') >>> l [1, 2, 3, [4, 5], 6, 'z'] >>> c [1, 2, 3, [4, 5], 6]
但是毕竟是浅拷贝,只是拷贝了最外层对象,没有拷贝子对象。
>>> id(l[3]) 39195784L >>> id(c[3]) 39195784L >>> l[3].append('az') >>> l [1, 2, 3, [4, 5, 'az'], 6, 'z'] >>> c [1, 2, 3, [4, 5, 'az'], 6]
果然原列表l
的子对象的内存地址和浅拷贝c
的对应子对象的内存地址一样,所以当原列表的子对象内容发生改变时,也会影响到c
。
深拷贝
有了浅拷贝的经验,直接造一个深拷贝对象d
,先查看一下外层对象和子对象的内存地址。
>>> l=[1,2,3,[4,5],6] >>> d=copy.deepcopy(l) >>> id(l) 39236296L >>> id(d) 39195912L >>> id(l[3]) 39179656L >>> id(d[3]) 39236040L
结果清晰的显示,原列表l
和深拷贝对象d
对应外层对象和子对象的内存地址均不同。
>>> l.append('z') >>> l [1, 2, 3, [4, 5], 6, 'z'] >>> d [1, 2, 3, [4, 5], 6] >>> l[3].append('az') >>> l [1, 2, 3, [4, 5, 'az'], 6, 'z'] >>> d [1, 2, 3, [4, 5], 6]
再查看一下结果,验证了我的猜想。
总结
Python是一门脚本语言,声明一个对象实际在内存中创建了一个地址存放对象,将对象名指向那个内存地址。用PHP的赋值方法进行赋值时,只是创建了一个新的对象名同时指向同一个内存地址。
py优雅语法的作者所用的列表拷贝方法c=l[:]
用的就是浅拷贝,只是写法相对于copy.copy()
更简洁。
通过Copy模块的代码可以发现deepcopy是在copy的基础上执行了递归。
# C:\Python27\Lib\copy.py def deepcopy(x, memo=None, _nil=[]): ... y = _reconstruct(x, rv, 1, memo); ... def _reconstruct(x, info, deep, memo=None): ... if deep: args = deepcopy(args, memo); ...
备注:本文发布于2017-08-03,我github page的原文地址。