详解python列表等对象的赋值和复制(浅拷贝copy()及深拷贝deepcopy()的使用区别与示例)

  • Post author:
  • Post category:python


python虽然没有指针的概念,但是对象、引用、地址这些内容还是存在的,尤其是像列表对象、某类型对象的引用上,搞清楚对象变量的复制和赋值很有必要,不然容易出现“莫名其妙”的错误。



认清对象、引用、地址(直接以“=”赋值变量)



以列表为例

python中给一个变量

a

赋值列表实际上是创建了一个列表对象,并将该列表的地址赋值给

a

,而变量a此时就成为了这个列表对象的引用。当用

a

给变量

b

赋值时,其实只是把这个列表对象的地址赋值给了b,即

a



b

都成为了该对象的引用,因此直接对

a



b

进行修改,都将使该对象发生变化,下面给出一段代码示例:

a = ["abc","bcd","cde",1,2,3]   #创建了一个对象并用变量a引用它
b = a                           #将a赋值给b,让b和a都同时作为列表对象的引用
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
b[0] = 0        #通过引用b将列表第一个元素赋值为0
a.remove(3)     #通过引用a将列表中第一个值为3的元素删除
print("修改后:")
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
a = ["a","new","list"]  #创建一个新的对象赋值给a,即让变量a成为新列表对象的引用
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址

运行结果为:

a= ['abc', 'bcd', 'cde', 1, 2, 3] b= ['abc', 'bcd', 'cde', 1, 2, 3]
a: <class 'list'> 2072617426752 b: <class 'list'> 2072617426752
修改后:
a= [0, 'bcd', 'cde', 1, 2] b= [0, 'bcd', 'cde', 1, 2]
a: <class 'list'> 2072617426752 b: <class 'list'> 2072617426752
a= ['a', 'new', 'list'] b= [0, 'bcd', 'cde', 1, 2]
a: <class 'list'> 2072617436224 b: <class 'list'> 2072617426752

可以看到,一开始a和b共同代表了创建的列表

["abc","bcd","cde",1,2,3]

,两个变量输出的内容、类型、地址都是完全一样的,用两个变量均可以对该列表进行修改,即

a



b

会互相影响。而后面给

a

另外赋值一个新的列表对象后,

a



b

就分别引用不同的对象了,两者在之后就不会互相影响了。



以自定义类为例

对于python中的类,包括自定义的类,所创建的对象也是类似的,给变量赋值仅仅是将对象地址给到变量,让变量成为该对象的引用,看下面的一段示例代码和结果就很好理解了:

class AClass:
    def __init__(self):
        self.alist = [1,2,3,"ab","cd"]
        self.aString = "abcdef"

a = AClass()
b = a
print("a:",a)
print("a.alist:",a.alist)
print("a.aString:",a.aString)
print("b:",b)
print("b.alist:",b.alist)
print("b.aString:",b.aString)
b.aString = 0
b.alist = 0
print("修改后:")
print("a:",a)
print("a.alist:",a.alist)
print("a.aString:",a.aString)

运行结果为:

a: <__main__.AClass object at 0x000002708399D4F0>
a.alist: [1, 2, 3, 'ab', 'cd']
a.aString: abcdef
b: <__main__.AClass object at 0x000002708399D4F0>
b.alist: [1, 2, 3, 'ab', 'cd']
b.aString: abcdef
修改后:
a: <__main__.AClass object at 0x000002708399D4F0>
a.alist: 0
a.aString: 0

可以看到,程序仅创建了一个自定义类的对象(地址为

0x000002708399D4F0

),变量

a



b

均是该对象的引用,所以通过

b

对其属性更改后,通过

a

输出内容便是更改后的内容。


综上,编程过程中一定要注意,尤其在很多时候我们可能需要临时使用并更改对象中的某些值但并不希望真的修改它以至于影响下次使用它,尤其针对某些其他语言转过来的人,可能习惯于直接用变量赋值,然后用新变量当临时变量来进行使用和修改,殊不知这样会直接影响原始对象的数据,造成一些未知的错误



浅拷贝(copy()函数的使用)

为了解决上面说的问题,有时候我们确实需要临时改变一下对象中的内容用于计算、统计等功能,但同时希望保持原有对象的数据不变,以便后面的流程中再次使用,所以就需要用到

对象的复制

,而不是上面所说的

变量的赋值



python中对象的复制可以用标准库

copy

中的

copy()

函数,使用前需要导入该库,示例如下:

import copy
a = copy.copy(b)


值得注意的是

,浅拷贝仅拷贝对象本身的内容并创建新对象,但是,

对象中的对象

即子对象仍然只是复制了地址而不是新创建同内容的子对象,下面会详细解释这种情况:



无嵌套的情况

先说无嵌套的情况,这种情况比较简单,沿用上面以列表为例的代码,仅作一处修改及将

b=a

改为

b=copy.copy()

,代码如下:

print("testing copy")
a = ["abc","bcd","cde",1,2,3]   #创建了一个对象并用变量a引用它
b = copy.copy(a)                #将a引用的对象浅拷贝给b
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
b[0] = 0        #通过引用b将列表第一个元素赋值为0
a.remove(3)     #通过引用a将列表中第一个值为3的元素删除
print("修改后:")
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
a = ["a","new","list"]  #创建一个新的对象赋值给a,即让变量a成为新列表对象的引用
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址

运行结果为:

a= ['abc', 'bcd', 'cde', 1, 2, 3] b= ['abc', 'bcd', 'cde', 1, 2, 3]
a: <class 'list'> 2370431356672 b: <class 'list'> 2370431370624
修改后:
a= ['abc', 'bcd', 'cde', 1, 2] b= [0, 'bcd', 'cde', 1, 2, 3]
a: <class 'list'> 2370431356672 b: <class 'list'> 2370431370624
a= ['a', 'new', 'list'] b= [0, 'bcd', 'cde', 1, 2, 3]
a: <class 'list'> 2370431369344 b: <class 'list'> 2370431370624

从结果可以看出,在复制(浅拷贝)之后,

a



b

虽然他们的内容是一样的,但是分别引用的两个不同的对象,各自的操作也互不影响。



有嵌套的情况

相对于无嵌套的情况,有嵌套的情况指的就是

对象中还包含有其他对象,例如,一个列表中,其中的元素也有列表;或者一个类的对象中还包含有别的对象,例如上面以自定义类为例的代码中,AClass类的对象中,其成员属性alist也是一个对象


在这种情况下,浅拷贝会出现什么情况呢?通过下面的代码示例就很容易理解了。



以列表为例

# 创建一个列表,列表中第2个和最后一个元素分别是一个字符串列表和整数列表
a = ["abc",["aa","bb","cc"],"cde",[1,2,3]]  
b = copy.copy(a)        #将a引用的对象浅拷贝给b
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
print("id of a[0]:",id(a[0]),"id of a[1]:",id(a[1]))
print("id of b[0]:",id(b[0]),"id of b[1]:",id(b[1]))
b[0] = 0     #通过引用b将列表第一个元素(字符串"abc")修改为0
a.pop(3)     #通过引用a将列表中索引为3的元素(即[1,2,3]整数列表)删除
b[1][0] = "new" #通过引用b将列表第二个元素所代表的字符串列表的第一个字符串"aa"修改为"new"
print("修改后:")
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
print("id of a[0]:",id(a[0]),"id of a[1]:",id(a[1]))
print("id of b[0]:",id(b[0]),"id of b[1]:",id(b[1]))

运行结果为:

a= ['abc', ['aa', 'bb', 'cc'], 'cde', [1, 2, 3]] b= ['abc', ['aa', 'bb', 'cc'], 'cde', [1, 2, 3]]
a: <class 'list'> 1928029320000 b: <class 'list'> 1928028905664
id of a[0]: 1927980678448 id of a[1]: 1928029321024
id of b[0]: 1927980678448 id of b[1]: 1928029321024
修改后:
a= ['abc', ['new', 'bb', 'cc'], 'cde'] b= [0, ['new', 'bb', 'cc'], 'cde', [1, 2, 3]]
a: <class 'list'> 1928029320000 b: <class 'list'> 1928028905664
id of a[0]: 1927980678448 id of a[1]: 1928029321024
id of b[0]: 140728955460320 id of b[1]: 1928029321024

从结果可以看出,在浅拷贝后,

a



b

的id是不一样的,意味着他们分别引用的是两个对象。通过后面的操作也可以看出,将

a

引用的对象最后一个元素删除,将

b

引用的对象的第一个元素修改为0,这两个操作互不影响。但同时也可以发现,

a[0]



b[0]



a[1]



b[1]

的id是一样的,这意味着

a[1]



b[1]

(注意这俩是子列表对象,而不是字符串或整数)引用的是同一个列表对象。所以:

1)当执行

b[0]=0

时,实际上是创建了一个新的值为0的Int对象(这就是所谓的“python中一切皆为对象”)并将其地址赋值给b[0],所以在这之后,我们发现

b[0]

的id已经发生了变化,跟

a[0]

的id不再一样了。

2)当执行

b[1][0]="new"

时,将

b[1]

所指向的子列表对象的第一个元素修改为”new”(即创建新对象并将地址赋值给

b[1][0]



b[1]

的第一个元素),而由于

b[1]



a[1]

是共同指向或引用地址为

1928029321024

的列表对象,所以

a



b

中的第二个元素(字符串子列表)共同发生了变化。



以自定义类为例

跟前面完全相同的类定义:

class AClass:
    def __init__(self):
        self.alist = [1,2,3,"ab","cd"]
        self.aString = "abcdef"

执行的示例代码基本与上述以自定义类为例的示例代码相同,仅修改两处(见注释):

a = AClass()
b = copy.copy(a)#修改等号赋值为浅拷贝
print("a:",a)
print("a.alist:",a.alist,id(a.alist))
print("a.aString:",a.aString)
print("b:",b)
print("b.alist:",b.alist,id(b.alist))
print("b.aString:",b.aString)
b.aString = 0
b.alist[0] = "a new"#仅修改alist属性的第一个元素
print("修改后:")
print("a:",a)
print("a.alist:",a.alist,id(a.alist))
print("a.aString:",a.aString)
print("b:",b)
print("b.alist:",b.alist,id(b.alist))
print("b.aString:",b.aString)

运行结果为:

a: <__main__.AClass object at 0x000001C0E77AC520>
a.alist: [1, 2, 3, 'ab', 'cd'] 1928028953216
a.aString: abcdef
b: <__main__.AClass object at 0x000001C0E77AC4C0>
b.alist: [1, 2, 3, 'ab', 'cd'] 1928028953216
b.aString: abcdef
修改后:
a: <__main__.AClass object at 0x000001C0E77AC520>
a.alist: ['a new', 2, 3, 'ab', 'cd'] 1928028953216
a.aString: abcdef
b: <__main__.AClass object at 0x000001C0E77AC4C0>
b.alist: ['a new', 2, 3, 'ab', 'cd'] 1928028953216
b.aString: 0

可以看到,运用浅拷贝复制后,

b



a

内容一样,但是id是不一样的,而

a.alist



b.alist

指向的列表id是一样的,所以在后面的修改中对

b.alist[0]

的修改,对

a



b

同时生效了,

b.aString=0

则是创建了值为0的新对象赋值给了

b.aString

,就只对

b

生效了。



深拷贝(deepcopy()函数的使用)

如果想完全复制一个全新的对象及其所有的子对象,则需要用深拷贝函数

deepcopy()

来完成,跟

copy()

函数一样,都来自

copy

库,导入和使用示例为:

import copy
a = copy.deepcopy(b)

沿用上面有嵌套的列表示例和自定义类示例,仅修改

copy()

函数为

deepcopy()

函数,代码如下:

import copy

print("以列表为例:")
a = ["abc",["aa","bb","cc"],"cde",[1,2,3]]  
b = copy.deepcopy(a)                #将a引用的对象浅拷贝给b
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
print("id of a[0]:",id(a[0]),"id of a[1]:",id(a[1]))
print("id of b[0]:",id(b[0]),"id of b[1]:",id(b[1]))
b[0] = 0     #通过引用b将列表第一个元素(字符串"abc")修改为0
a.pop(3)     #通过引用a将列表中索引为3的元素(即[1,2,3]整数列表)删除
b[1][0] = "new" #通过引用b将列表第二个元素所代表的字符串列表的第一个字符串"aa"修改为"new"
print("修改后:")
print("a=",a,"b=",b)    #输出a,b列表中的内容
print("a:",type(a),id(a),"b:",type(b),id(b))#输出a,b的类型和地址
print("id of a[0]:",id(a[0]),"id of a[1]:",id(a[1]))
print("id of b[0]:",id(b[0]),"id of b[1]:",id(b[1]))

print("以自定义类为例:")
class AClass:
    def __init__(self):
        self.alist = [1,2,3,"ab","cd"]
        self.aString = "abcdef"
        
a = AClass()
b = copy.deepcopy(a)
print("a:",a)
print("a.alist:",a.alist,id(a.alist))
print("a.aString:",a.aString)
print("b:",b)
print("b.alist:",b.alist,id(b.alist))
print("b.aString:",b.aString)
b.aString = 0
b.alist[0] = "a new"
print("修改后:")
print("a:",a)
print("a.alist:",a.alist,id(a.alist))
print("a.aString:",a.aString)
print("b:",b)
print("b.alist:",b.alist,id(b.alist))
print("b.aString:",b.aString)

运行结果如下:

以列表为例:
a= ['abc', ['aa', 'bb', 'cc'], 'cde', [1, 2, 3]] b= ['abc', ['aa', 'bb', 'cc'], 'cde', [1, 2, 3]]
a: <class 'list'> 2194904591232 b: <class 'list'> 2194905204032
id of a[0]: 2194856443120 id of a[1]: 2194904652928
id of b[0]: 2194856443120 id of b[1]: 2194904632960
修改后:
a= ['abc', ['aa', 'bb', 'cc'], 'cde'] b= [0, ['new', 'bb', 'cc'], 'cde', [1, 2, 3]]
a: <class 'list'> 2194904591232 b: <class 'list'> 2194905204032
id of a[0]: 2194856443120 id of a[1]: 2194904652928
id of b[0]: 140728955460320 id of b[1]: 2194904632960
以自定义类为例:
a: <__main__.AClass object at 0x000001FF0A82D580>
a.alist: [1, 2, 3, 'ab', 'cd'] 2194904589504
a.aString: abcdef
b: <__main__.AClass object at 0x000001FF0A82D670>
b.alist: [1, 2, 3, 'ab', 'cd'] 2194904591232
b.aString: abcdef
修改后:
a: <__main__.AClass object at 0x000001FF0A82D580>
a.alist: [1, 2, 3, 'ab', 'cd'] 2194904589504
a.aString: abcdef
b: <__main__.AClass object at 0x000001FF0A82D670>
b.alist: ['a new', 2, 3, 'ab', 'cd'] 2194904591232
b.aString: 0

结果就不做过多解释了,一句话,深拷贝后得到的对象,跟之前的对象内容完全一样,但是完全没有任何关联,任何更改都不影响原来的对象。当然,这种做法简单粗暴,如果对象占用资源较多,深拷贝会增加资源的消耗,资深人员可以根据需要灵活使用深拷贝浅拷贝。



版权声明:本文为yl19870518原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。