Python 赋值、深浅拷贝介绍与简要实现以及在数组扩展的潜在问题
1· 赋值与深浅拷贝
1.1 数据的存储与类型
Python语言和其他高级语言一样,变量是对内存及其地址的抽象。Python中万物皆对象,变量存储的是其地址(引用)而非值本身。
Python的数据类型,我一般看做基本类型和复合类型。基本类型就是int, long, bool, str等,复合类型则是各种数据结构如list, tuple, dict, set等。
1.2 赋值
对于基本的数据类型,被初始化时会分配一块内存空间,由于基本数据类型的初始化都是赋值一个常量,基本数据类型的变量则对应了一个存储了常量的空间。对基本数据类型进行赋值,等价于重新开辟一块内存空间保存新的值,并将变量重新指向新的空间,如果需要则回收旧的空间。如下图所示首先对整型变量A赋值0,然后赋值B=A,再更改A的值,由于是基本数据类型,变量本身指向对保存常量的空间,更改了A只是改变了A的指向,不影响B。
对于复合类型这样的各种数据结构,其内部每一个元素都可以认为是一个引用,指向某一块区域,如果这个元素是基本类型的,那么同样指向一个存储了常量的内存空间,否则可以认为指向了另一个引用。用常量对复合类型进行赋值,等于建立一个引用的列表,该复合变量的每个元素都确定了指向;但使用已有的变量进行赋值,则相当于将新变量指向了原来变量的空间:
这时list A和B指向同一item addr 的列表,如果通过索引复制的方式修改,实际上是让某个item指向另一个空间,但是这两个list实际对应的仍是同一存储区域,修改一个,另一个也会跟着变。
对于复杂的数据结构类型,赋值就等于共享了资源,双方都可以修改共同的内存空间,修改对双方都有影响。
1.3 浅拷贝
如果我们需要对原始数据留存一个副本,或者不希望双方互相影响,那么赋值就不合适了,这时就需要拷贝来实现。拷贝操作是通过copy模块进行的。浅拷贝也就是一般的拷贝,使用的是copy.copy()函数。
浅拷贝的“浅”是指,无论数据结构是怎样的,都只拷贝一层:
如图所示,当使用B=copy.copy(A)时,列表第一层的引用列表被复制了一个副本,二者之间是互不干扰的,如果改变A中某个元素的指向,B不会受影响。这一点对于单层的数据结构可以做到完全的独立:
但对于多层的嵌套结构,由于只有第一层是独立的,如果修改了深层的数据,由于双方仍然指向的是同一内存空间,依然是会有影响的:
这里a[3]与b[3]都是列表,通过浅拷贝二者的引用互相独立,但是仍然指向的是同一个空间。
1.4 深拷贝
深拷贝的使用是通过copy模块的copy.deepcopy()函数。深拷贝是指无论多复杂的数据结构,只要元素指向的不是常量空间(元素不是基本类型)就进行复制,将引用独立开来:
深拷贝实现了完全的相互独立,无论多复杂的数据结构,都不会相互影响。
2· 简易的实现
这里我们讨论比较简单的一种情况,就是list的嵌套。被拷贝的对象要么是基本类型,要么是list或多层的list嵌套。由于对于基本类型,赋值操作符 “=” 本身即可以认为是深拷贝的,因此对于基本类型元素,直接赋值并返回即可,对于list,可以递归调用这个函数即可:
import os
import sys
def myDeepCopy(a):
if type(a) != list:
b=a
else:
b=[]
for a_i in a:
b.append(myDeepCopy(a_i))
return b
if __name__ == '__main__':
a = input()
b = myDeepCopy(a)
print("a= {},b= {}".format(a,b))
a=input()
print("a= {},b= {}".format(a,b))
这里举个简单的例子,myDeepCopy函数输入一个对象a,返回他的深拷贝结果b。如果a的类型不是list,对于当前的场景,那就是一些基本类型,直接赋值返回即可;如果是list,就对其中每个元素调用myDeepCopy方法即可。
测试如下:首先输入a,调用myDeepCopy(a)得到深拷贝结果b,打印a,b可以看到a,b是一样的;接着在输入一个新的a,再次打印,可以看到a更新了,b没有变化。
3· 不容易发现的问题
3.1 自定义类的赋值
例如如下的自定义类:
class testClass:
def __init__(self,a=0,b=[1,2],c='hello'):
print("constructor")
self.a = a
self.b = b
self.c = c
if __name__ == '__main__':
A = testClass(0,[1,2],'A')
B = A
C = copy.copy(A)
D = copy.deepcopy(A)
print("B:{},{},{}".format(B.a,B.b,B.c))
print("C:{},{},{}".format(C.a,C.b,C.c))
print("D:{},{},{}".format(D.a,D.b,D.c))
A.b[0]=100
print("B:{},{},{}".format(B.a,B.b,B.c))
print("C:{},{},{}".format(C.a,C.b,C.c))
print("D:{},{},{}".format(D.a,D.b,D.c))
构造对象A,并B = A,C和D分别是A的浅拷贝与深拷贝,这时ABCD的成员变量值是一样的,此时修改A的成员变量,观察发现。自定义类的赋值、深浅拷贝与复合数据类型是一致的。
3.2 Python快速数组扩展
Python的乘号*可以用于数组的快速扩展,例如:
a=[1,2,3]
b=a*3
那么我们会得到:
这时如果对b中的元素进行赋值或修改,如:
b[0]=10
则有
很正常的修改。
但是如果此时a数组是嵌套的list,情况就会有所不同:
可以看到当a数嵌套list时,通过*进行数组的扩展,得到的b也是将a重复了若干次的结果,但此时修改b中第一个数组的第一个元素,却会将每一个“循环”内的第一个数组第一个元素都修改掉。
这是因为利用乘号进行数据扩展时,被扩展部分实际上是浅拷贝的,只拷贝了第一层的数据结构。当元素是整数这样的基本类型(最底层)时,会开辟新的存储空间;而当元素是list等类型时,相当于指向了原有的空间,因此改动一个值,其他的都会跟着变化。
实际上b=a*3
这样的扩展后赋值语句本身也是浅拷贝:
修改a之后b中对应元素的值跟着变化,因为本质都指向同一块内存空间。
这种乘法扩展数组的方法非常方便,但要注意本质是浅拷贝,避免出错。