Python的复制
对于Python的复制,相信很多人都有疑惑,因为Python的默认复制是浅复制实现的,因此经常出现各种离奇的问题。要理解好Python的复制机制,需要先理解Python的变量和类型。
不可变对象与可变对象
在Python中,对象可以分为如下两种类型:不可变对象和可变对象。其中不可变对象包括int,float,long,str和tuple等,可变对象则包括list、dict和set等,值得注意的是:这里说的不可变对象中的不可变指的是值不可变。
对于不可变类型的变量,如果要更改变量,则会创建一个新值,把变量绑定到新值上,而旧值如果没有被引用就等待垃圾回收。另外,不可变的类型可以计算hash值,作为字典的key。可变类型数据对对象操作的时候,不需要再在其他地方申请内存,只需要在此对象后面连续申请(+/-)即可,也就是它的内存地址会保持不变,但区域会变长或者变短。这个机制可通过如下代码理解:
num = 1
print id(num)
num += 2
print id(num)
l = [1, 2, 3]
print id(l)
l.append(4)
print id(l)
该例子中num的两个id是不一样的,表明num变量在不同的取值下指向的内存地址不一样;而l的两个id是一样的,表明l变量在不同的取值下指向的内存地址是一样的。
变量和对象
在Python中,非常重要的一点:“变量无类型,对象有类型”。此点的理解是:Python中的变量是无类型的,但Python是区分类型的。Python的每一个变量其实都是指向内存对象的一个指针,变量都是值得引用,而类型只与对象有关。总结来说:在Python中,类型是属于对象的,而不是变量, 变量和对象是分离的,对象是内存中储存数据的实体,变量则是指向对象的指针。总的来说,Python中类型是属于对象的,变量并没有类型,变量和对象其实是相互分离的,对象是内存中存储数据的实体,而变量只是指向对象的指针。
在不可变对象和可变对象的代码中,可能读者有个疑问,如下代码会改变变量l的id,
l = [1, 2, 3]
print id(l)
l = [1, 2, 3, 4]
print id(l)
的确,上述代码l的两个id不一样,这其中的区别在于开始l指向了[1, 2, 3]的list的对象,当使用append方法时,由于l是可变对象,因此不需要重新再内存中创建新的对象,只需在原来的内存位置增加空间即可。而当使用“=”赋值一个list时,其实在内存是先生成了一个[1, 2, 3, 4]的list对象,然后让l指向该对象,因此出现两个id不一样。
函数的参数传递方式
对于函数的参数传递方式,其实只要理解好变量和对象这一节的内容就很容易理解了,因为Python中变量都是指向对象的指针,而对象才是内存中存储数据的实体,因此函数的参数传递实质上就是让变量指向传入的对象而已。
理解好下面的代码就基本理解了函数的参数传递方式了,下面逐一讲解结果以及原因,
def fun_add_num(num):
num += 2
def fun_add_list(l):
l.append(4)
def fun_assign_list(l_1):
l_1 = [1, 2, 3, 4]
if "__main__" == __name__:
num = 1
print num
fun_add_num(num)
print num
l = [1, 2, 3]
print l
fun_add_list(l)
print l
l_1 = [1, 2, 3]
print l_1
fun_assign_list(l_1)
print l_1
(1)在main函数中两次输出的num的值均为1,在fun_add_num中的num开始是指向1这个int对象的,但当执行加法时,由于int对象是不可变对象,因此在fun_add_num中的num变量就变为指向3这个int对象,而main中的num变量依旧指向1这个对象,因此两次输出的num的值均为1
(2)在main函数中两次输出的l的值依次为[1, 2, 3],[1, 2, 3, 4],在fun_add_list中的l开始是指向[1, 2, 3]这个list对象,当使用append方法时,需要改变该对象的值,由于list是可变对象,因此只需要在其后面增加内存空间即可,从而得到这样的结果。
(3)在main函数中两次输出的l_1的值依次为[1, 2, 3],[1, 2, 3],在fun_assign_list中的l_1开始时指向[1, 2, 3]对象,该对象与main函数中l_1指向的对象一致,但当使用赋值时,[1, 2, 3, 4]这个list会在内存中重新申请空间存储,因此此时在在fun_assign_list中的l_1改为指向[1, 2, 3, 4]这个list对象,该对象与main函数中生成的[1, 2, 3]对象没有任何关系,因此出现这样的结果。
浅拷贝和深拷贝
本人这部分主要参考了下面这个博客,读者可以自行前往学习:
(1)变量赋值
father = ["father", ["python", "c++"]]
son = father
print id(father)
print father
print [id(ele) for ele in father]
print id(son)
print son
print [id(ele) for ele in son]
father[0] = "father_0"
father[1].append("java")
print id(father)
print father
print [id(ele) for ele in father]
print id(son)
print son
print [id(ele) for ele in son]
运行结果:
解析:
使用变量赋值的方式进行复制时,实质上就是让father变量和son变量指向同一个内存地址(这点可以从两者的id一致验证),因此当改变father指向的list对象的元素时,son同样会改变。
(2)浅拷贝
import copy
father = ["father", ["python", "c++"]]
son = copy.copy(father)
print id(father)
print father
print [id(ele) for ele in father]
print id(son)
print son
print [id(ele) for ele in son]
father[0] = "father_0"
father[1].append("java")
print id(father)
print father
print [id(ele) for ele in father]
print id(son)
print son
print [id(ele) for ele in son]
运行结果:
解析:
当使用浅复制的方式进行复制时,可以看到father和son两个变量指向的内存地址是不一样的,但两个内存地址的对象的元素指向的地址是一致的。由于”father”的类型是str,为不可变对象,当对象的值需要改变时,重新申请内存空间放置,因此此时father[0]变量指向的地址改变了,而son[0]依旧没有改变,所以father[0]变量指向的内存地址的对象为”father_0”,而son[0]则依旧指向”father”对象。由于father[1]指向的是list对象,该对象为可变对象,因此使用append等方法修改该对象值时只需在原内存空间上增加空间,father[1]指向的地址不需要变,由于son[1]与father[1]指向的地址是一致的,因此会出现father[1]和son[1]同时改变的现象。
(3)深拷贝
import copy
father = ["father", ["python", "c++"]]
son = copy.deepcopy(father)
print id(father)
print father
print [id(ele) for ele in father]
print id(son)
print son
print [id(ele) for ele in son]
father[0] = "father_0"
father[1].append("java")
print id(father)
print father
print [id(ele) for ele in father]
print id(son)
print son
print [id(ele) for ele in son]
运行结果:
解析:
当使用深复制进行复制时,不仅father和son变量指向的地址不一致,就连father和son的元素指向的地址也不一定一致,不一定一致的原因在于,在python中,相同值得不可变对象的内存地址均一致,因此导致开始时father[0]和son[0]指向了相同的内存地址,但当father[0]指向的对象的值需要改变时,father[0]指向了另外一个地址,此时father[0]和son[0]指向的内存地址就不一致了;而相同值得可变对象的内存则可以不一致,因此开始时father[1]和son[1]指向的内存地址不一致,当father[1]指向的内存对象值改变时,不会影响到son[1]指向的内存对象值。