Python对象的拷贝

Python011

Python对象的拷贝,第1张

Python赋值操作或函数参数传递,传递的永远是对象引用(即内存地址),而不是对象内容。在Python中一切皆对象,对象又分为可变(mutable)和不可变(immutable)两种类型。对象拷贝是指在内存中创建新的对象,产生新的内存地址。当顶层对象和它的子元素对象全都是immutable不可变对象时,不存在被拷贝,因为没有产生新对象。浅拷贝(Shallow Copy),拷贝顶层对象,但不会拷贝内部的子元素对象。深拷贝(Deep Copy),递归拷贝顶层对象,以及它内部的子元素对象。

Python中一切皆对象,对象就像一个塑料盒子, 里面装的是数据。对象有不同类型,例如布尔型和整型,类型决定了可以对它进行的操作。现实生活中的"陶器"会暗含一些信息(例如它可能很重且易碎,注意不要掉到地上)。

对象的类型还决定了它装着的数据是允许被修改的变量(可变的mutable)还是不可被修改的常量(不可变的immutable)。你可以把不可变对象想象成一个透明但封闭的盒子:你可以看到里面装的数据,但是无法改变它。类似地,可变对象就像一个开着口的盒子,你不仅可以看到里面的数据,还可以拿出来修改它,但你无法改变这个盒子本身,即你无法改变对象的类型。

对象拷贝是指在内存中创建新的对象,产生新的内存地址。

浅拷贝(Shallow Copy),拷贝顶层对象,但不会拷贝内部的子元素对象。

2.1.1. 顶层是mutable,子元素全是immutable

当顶层对象是mutable可变对象,但是它的子元素对象全都是immutable不可变对象时,如[1, 'world', 2]

① 创建列表对象并赋值给变量a

② 导入copy模块,使用copy.copy()函数浅拷贝a,并赋值给变量b

③ 修改变量a的子元素a[0] = 3,由于整数是不可变对象,所以并不是修改1变为3,而是更改a[0]指向对象3

当顶层对象是 mutable可变对象 ,但子元素也存在 mutable可变对象 时,如 [1, 2, ['hello','world']]

① 浅拷贝 copy.copy() 只拷贝了顶层对象,没有拷贝子元素对象['hello','world'],即a[2]和b[2]指向同一个列表对象

② 修改a[2][1] = 'china',则b[2][1] = 'china'

当顶层对象是immutable不可变对象,同时它的子元素对象也全都是immutable不可变对象时,如(1, 2, 3)

变量a与变量b指向的是同一个元组对象,没有拷贝

当顶层对象是immutable不可变对象时,但子元素存在mutable可变对象时,如(1, 2, ['hello','world'])

变量a与变量b指向的是相同的元组对象,并且a[2]与b[2]指向同一个列表,所以修改a[2][1]会影响b[2][1]

深拷贝(Deep Copy),递归拷贝顶层对象,以及它内部的子元素对象

当顶层对象是mutable可变对象,但是它的子元素对象全都是immutable不可变对象时,如[1, 'world', 2]

变量a与变量b指向不同的列表对象,修改a[0]只是将列表a的第一个元素重新指向新对象,不会影响b[0]

当顶层对象是mutable可变对象,但子元素也存在mutable可变对象时,如[1, 2, ['hello','world']]

深拷贝既拷贝了顶层对象,又递归拷贝了子元素对象,所以a[2]与b[2]指向了两个不同的列表对象(但是列表对象的子元素初始指定的字符串对象一样),修改a[2][1] = 'china'后,它重新指向了新的字符串对象(内存地址为140531581905808),不会影响到b[2][1]

当顶层对象是immutable不可变对象,同时它的子元素对象也全都是immutable不可变对象时,如(1, 2, 3)

变量a与变量b指向的是同一个元组对象,不存在拷贝

当顶层对象是immutable不可变对象时,但子元素存在mutable可变对象时,如(1, 2, ['hello','world'])

变量a与变量b指向的是不同的元组对象,同时a[2]与b[2]指向不同的列表对象,所以修改a[2][1]不会影响b[2][1]

使用=是赋值,即将列表对象的引用也赋值给变量b,可以将列表对象想像成一个盒子,变量a相当于这个盒子上的标签,执行b = a后,相当于再在这个盒子上贴上b标签,a和b实际上指向的是同一个对象。因此,无论我们是通过a还是通过b来修改列表的内容,其结果都会作用于双方。

b/c/d都是a的复制,它们都指向了不同的列表对象,但是没有拷贝子元素,a[2]和b[2]/c[2]/d[2]指向同一个列表, 相当于浅拷贝的效果

使用分片[:]操作,a和b其实是指向同一个元组,而且没有拷贝子元素,a[2]和b[2]也指向同一个列表,相当于浅拷贝的效果

同列表类似,可以使用字典的copy()函数或者转换函数dict()

变量a与变量b/c指向不同的字典,但是没有拷贝子元素,a['jobs']和b['jobs']/c['jobs']指定同一个列表, 相当于浅拷贝的效果

同列表类似,可以使用集合的copy()函数或者转换函数set()

变量a与变量b/c指向不同的集合,而集合的元素必须是hashable,所以修改集合a不会影响到b/c

对于Python的初学者,在对象的使用过程中,由于对变量的赋值和对象的复制中的概念模糊,导致程序出错。

例如,下面的代码:

输出结果为:

a = [6,2,3,4,5],

b = [6,2,3,4,5],

c = [1,2,3,4,5]

a等于b?True

a等于c?True

a是b?True

a是c? False

可以看到,a,b, c所指向的对象的值都相同(a==b为True). a和b都是代表同一个对象(a is b为True)。当我们通过变量b对该列表进行修改时,由于a也指向该列表,所以当打印a,b时,我们得到相同的值。 而a和c则是代表不同的对象(a is c为False),所以修改b所指向得列表不会改变c梭子乡的列表的值.

在Python中,所有的变量都代表了对象,即便是简单的数字类型(int, float, bool),也是以对象的形式存在的。我们看下面的代码:

输出结果是:

a==b为True

a is b为True

可见,a, b都是指向同一个对象。接下来,进行下面的操作,

输出结果是:

a = 1, b = 2

a is b为False

与前面的列表不同,当我们对b做修改时,实际上是给b赋予了一个新生成的对象,对数值类型来说,所有的数值运算都会创建一个数值对象,并将这个对象指定给变量。因此,a与b指向了不同的对象,数值也不同。

再回过头来看列表对象,

我们知道,b是与a指向同一对象的变量,使用b对该对象进行修改,与使用a对该对象进行修改,效果是完全一样的。如果我们需要需要一个与a完全相同又与a相互独立的列表,那么就需要复制这个对象,也就是新建一个内容和源对象相同的对象。

对于列表来说,最简单的复制方法是通过下标索引的方式创建新的列表:

对于各种数据类型通用的对象拷贝复制,我们可以使用python内建的copy模块。

对于复杂对象(如嵌套列表)的复制,则需要注意区分浅拷贝和深拷贝。我们来看下面的代码:

得到的结果是:

a[0] is b[0]为 True

a[0] is c[0]为 False

a = [[-1, 2, 3], [4, 5, 6]]

b = [[-1, 2, 3], [7, 8, 9]]

c = [[1, 2, 3], [4, 5, 6]]

a[1] is b[1]为False

从上面的代码我们可以看到,copy函数为浅拷贝,只拷贝了对象的外层,而对象内部所包含的对象仍然指向原有的对象。而deepcopy则为深拷贝,对象内部的对象也进行了复制。

以上我们对变量的赋值和对象的复制做了更加深入的分析。在具体的使用中,我们需要根据具体来决定使用赋值、浅拷贝、深拷贝。

python的shutil模块提供了便捷的复制文件命令

shutil.copy(srcfile,dstfile)  #拷贝文件,目标文件必须存在,否则就会报错

shutil.copyfile(srcfile,dstfile)  #拷贝文件,目标 文件无需存在

shutil.copytree(srcdir,dstdir)    #srcdir为源目录,dstdir为目标目录,复制时,如果dstdir已经存在时,会报FileExistsError错误,提示“当文件已存在时,无法创建该文件”

shutil.copymode(src, dst)     # 仅拷贝权限。内容、组、用户均不变,目标文件必须存在