写在前面
已经见多很多这样的Blog,为什么还想自己写呢?原因很简单,每当学到一个知识时候,尝试着对自己或者别人讲解这个知识点,当别人能听懂的时候,就算是真的理解了。
重要部分
首先,要分清楚id()函数、is和= =运算符 的使用以及区别。id()是返回一个对象的地址;is 比较两个变量的引用是否指向同一个对象;= = 比较两个变量的值是否相等
其次,Python中一切皆对象,所有的对象拥有三个属性:id、type、value,Python对象分两种类型,一是不可变对象,二是可变对象。
不可变对象:该对象所指向的内存中的值不能被改变。一个变量指向的是不可变对象的引用,当对这个变量的值做修改时,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,这时变量的指向已经发生改变,由原来的指向新开辟的地址。
可变对象:该对象所指向的内存中的值可以被改变。变量改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,变量指向的地址并没有发生改变。
a = 100 #id(a) ->140736904075200
a += 10 #id(a) ->140736904078400
L = ['C#', 'C++', 'Java'] #id(L) ->1627814636224
L.append('Python') #id(L) ->1627814636224
常见的不可变对象:数字、字符串、元组
常见的可变对象:列表、字典、集合
接下来,看一下这个例子
import copy
is_same_addr = lambda v1,v2:(id(v1)==id(v2))
e = 10 #e指向不可变对象
v_1 = e #直接赋值
v_2 = copy.copy(e) #浅拷贝
print is_same_addr(v_1, e) #>> True
print is_same_addr(v_2, e) #>> True
显然,当变量e指向的是不可变对象,采用直接赋值或者拷贝的方式让另一个变量v等于e,实际上两个变量指向的是同一个变量的引用,并没有复制出新的副本
接着,再往下看
e = ['e','jianl', ['Python', 'C++']]
v_1 = e #1 赋值
v_2 = e[:] #2 切片
v_3 = copy.copy(e) #3 浅拷贝
v_4 = copy.deepcopy(e) #4 深拷贝
print is_same_addr(v_1, e) #>> True
print is_same_addr(v_2, e) #>> False
print is_same_addr(v_3, e) #>> False
print is_same_addr(v_4, e) #>> False
可以看出,除了赋值的方式是复制变量e的引用外, 其余方式都是有开辟新的内存,不算是 ‘假复制’,
因此,我想如果v_2 、v_3、 v_4 不小心发生改变,应该不会影响原变量e的值,依据是他们所指向并不是同一个对象, 真的是这样吗?敲代码看一下
v_2[0] = 'v_2'
v_3[0] = 'v_3'
v_4[0] = 'v_4'
# 输出各个变量的值
>> e : ['e', 'jianl', ['Python', 'C++']]
>> v_1: ['e', 'jianl', ['Python', 'C++']]
>
>> v_2: ['v_2', 'jianl', ['Python', 'C++']]
>> v_3: ['v_3', 'jianl', ['Python', 'C++']]
>> v_4: ['v_4', 'jianl', ['Python', 'C++']]
似乎是这样,不过到这里还没完,接着试一下对列表里的可变对象修改操作,看下结果又会怎样呢?
v_2[2][0] = 'C#'
v_3[2][0] = 'C#'
分别对v_2[2] 、v_3[2]所指向的列表作同样的修改,两种情况输出的结果一致,而且变量e的值被修改了
e : ['e', 'jianl', ['C#', 'C++']]
v_1: ['e', 'jianl', ['C#', 'C++']]
v_2: ['e', 'jianl', ['C#', 'C++']]
v_3: ['e', 'jianl', ['C#', 'C++']]
v_4: ['e', 'jianl', ['Python', 'C++']]
同样,再对v_4[2]所指向的列表作同样的修改
v_4[2][0] = 'C#'
结果,只有v_4的值被修改了
e : ['e', 'jianl', ['Python', 'C++']]
v_1: ['e', 'jianl', ['Python', 'C++']]
v_2: ['e', 'jianl', ['Python', 'C++']]
v_3: ['e', 'jianl', ['Python', 'C++']]
v_4: ['e', 'jianl', ['C#', 'C++']]
为什么会这样呢?来做一个验证, 对比变量指向的对象成员中为可变变量的地址
print is_same_addr(v_1, e),is_same_addr(v_1[2], e[2]) #True True
print is_same_addr(v_2, e),is_same_addr(v_2[2], e[2]) #False True
print is_same_addr(v_3, e),is_same_addr(v_3[2], e[2]) #False True
print is_same_addr(v_3, e),is_same_addr(v_4[2], e[2]) #False False
水落石出,利用切片[:]属性或者copy()函数实现对一个变量赋值,属于浅拷贝,只是拷贝对象的一部分不可变类型的数据,对于对象里的可变类型对象属性,仅仅是拷贝一个引用;而使用deepcopy()函数实现的是深拷贝,将变量所指向的对象整个复制,开辟新的空间,再由另一个变量指向这块新的空间。
最后
几句话总结:
1 对象有类型,变量无类型,变量是对象的标签;
2 对指向不可变对象变量的值作修改,实际上是改变变量的指向,对象本身是不可能被修改的;指向可变对象变量的值发生改变,实际上是对象本身被修改了;
3 v = e,实际上是将e的引用赋值给v, v e指向同一个变量;
4 v= e[:] 及 v=copy(e),浅拷贝,只复制部分数据,而e所指向的对象中可变类型属性,只是拷贝引用;
5 v = deepcopy(e), 深拷贝,e指向的整个对象被复制,再由v指向;