对象引用,可变性及复制
我们知道python是面向对象编程的语言,python里的变量是引用式变量,即是对对象的一个引用。
变量是标注,而不是盒子
— Fluent Python
变量不是盒子
如以下代码所示:
a = "对象"
b = a
a +="我是后加上去的对象"
print(b)
#输出:"对象我是后加上去的对象"
由此可见,我们改变了a的内容,b也随着一起改变了,这就说明b与a所指的是同一个对象,这就好比一个女生A有两个男朋友甲和乙,小甲管A叫静静,小乙管A叫丽丽,但他们两个的对象是同一个人。因为id值是变量的唯一数值标注,自然静静与丽丽的id也是相同的。
创建对象之后才会把变量分配给对象
我们需要注意的一点是python中是先创建对象然后再有变量,我们通过以下这个例子来理解一下。
class Jingjing:
def __init__(self):
print(" Jingjing id: %d" %id(self))
x = Jingjing()
y = Jingjing()*10
Traceback (most recent call last):
File "<pyshell#7>", line 1, in <module>
y = Jingjing()*10
TypeError: unsupported operand type(s) for *: 'Jingjing' and 'int'
我们看到对象是不能用*运算符的,
元组的相对不可变性
我们经常会在一些书中看到,python的元组是不可变的。这句话只说对了一半,其实,元组与python中的列表,字典一样,保存的是关于对象的引用。所以python的元组不可变的更为准确的说法是python中元组的引用不可变,但引用所指的对象可变(当所指对象是可变对象时),如下的例子将会展示这种神奇的性质。
tup1 =(1,2,3,[4,5])
>>>type(tup1)
output: <class 'tuple'>
>>>tup1[3].append(6) #现在我们对[4,5]进行修改
>>> tup1
output: (1, 2, 3, [4, 5, 6]) #咦! 我们发现元组tup1改变了
>>>tup1[2] = 4 #现在我们对元组中的3进行修改
程序报错:
TypeError: 'tuple' object does not support item assignment
现在,让我们来分析一下这个例子。刚开始我们尝试修改元组中的列表[4,5],成功修改成[4,5,6],这是因为tup1元组中保存的是对列表[4,5]的引用(说白了保存的就是图中指向列表的蓝箭头),在修改的过程中,我们实际上是对列表本身进行修改,而并未影响到元组中指向列表的引用。
下图形象的展示了如何是修改的:
同理啊,这张图片也解释了为什么后来修改元组中的3会报错,因为int类型是不可变的,修改成别的那么元组下标为2的位置的引用就改变了。
关于复制的问题
浅复制(shallow copy)
经过了以上对元组的深入分析,我们了解到了对于元组保存在他之中的其实是列表的引用。既然谈到了引用,那么有没有思考过一个问题,我们经常会对变量进行复制,那么对于变量中的内置可变类型是重新开辟内存空间生成一个还是向上文元组那样,与引用有关呢? 答案是与二者都有关系,且听我慢慢道来。
**在python中,如果没有特殊声明的话,是默认做浅复制的:即只是复制了外层的容器,其中的内容还是对原容器的元素的引用。**我们来看下面的例子:
>>>l1 = [2,[3,4,5],(66,67)]
>>>l2 = l1.copy()
>>>print(l1 == l2)
output: True
>>>print(l1 is l2)
output: False
可以看到,这就是l1,l2在浅复制中的细节。不光是用copy()方法,我们使用l2 = list(l1); l2 = l1[:],都是一样的效果。
理解这种引用的传递对我们明晰复制的概念至关重要,我们在通过一次操作来加深理解。好,在上述浅复制的基础上,如果再执行以下代码,那么它的程序状态图应该怎么画呢?
l1[1].remove(5)
l2[2]+ =(1,2)
结果:
显然,这和我们脑海中最初想要的结果有可能不大吻合,为什么元组不就地改变呢,而是创建了一个新的元组?这是因为+= 对于元组来说,并不会就地修改本身,他会创建一个新的元组,重新绑定给l2[2]。等同于 l2[2] =l2[2] + (1,2)
浅复制操作简单,便捷,但为了避免有时在复制的时候出现意料之外的错误,我们来看看深复制是怎样运作的。
深复制(deepcopy)
什么是深复制,即副本不共享对内部对象的引用,也就是说我们所引用的对象值是相同的,但他们的id不一样。
import copy
l1 = [1,2,3,[123,213]]
l2 = copy.deepcopy(l1)
print("id l1,l2:", id(l1),id(l2))
output: id l1,l2: 2666819183624 2666815939464
到这里,深复制和浅复制的区别就很清晰了。 但是在深复制当中有一个坑,这使我困惑了好几天。大家来看一下这段代码:
import copy
l1 =[1,2,[34,56],(77,88)]
l2 = copy.deepcopy(l1)
print("id:l1[3],l2[3]", id(l1[3]),id(l2[3]))
output: id:l1,l2 2285897024968 2285897024968
咦?不是说深复制吗,那为什么列表中所包含对元组的引用相同呢?我们看到和上面的l1相比,列表中多了一个内置的元组。这里我们要探讨的就是什么情况发生深复制元组引用相同。
情况1:当元组中的对象是不可变时,pyhton不会复制元组
如上所示
情况2: 当元组中包含可变对象时,python会复制一个相同元组,但二者id不相同。
import copy
l1 =[1,2,(77,"我",[56,57])]
l2 = copy.deepcopy(l1)
print("id,l1[2],l2[2]:",id(l1[2]),id(l2[2]))
output: id,l1[2],l2[2]: 2319530312152 2319530476312
好了,这就是深复制的基本内容,如果我们想控制 copy和deepcopy的行为,我们可以实现__copy__ 和 __deepcopy__方法,有兴趣的朋友可以参照python官方文档https://docs.python.org/3/library/copy.html