对象引用、可变性[流畅的Python]
文章目录
记录一下在读《流程的python》时对自己有用的知识点。
1. 变量不是盒子
进行赋值操作的时候,并不是将一个对象放在变量构成的盒子
中。实际上,在进行赋值操作的时候,等号右边的对象是在它被创建之后才把等号左边的变量分配给它,相当于给这个对象贴上了一个标签🏷️。
|
|
可以看到,在将变量x分配给类Gizmo的一个实例之前,Gizmo的实例已经被创建。
2. 标识、相等性和别名
正如上边所说,变量相当于是一个对象的标签,当然,一个对象可以被贴上许多标签,即多个变量绑定同一个对象。
|
|
可以看到list1和list2是同一个对象的不同标签。 但是,如何才能判断两个变量是否绑定的是同一个对象?
|
|
接着上边的例子,我们现在有一个叫做list3的变量,它和list1、list2是相等的,但是它们三个绑定的是同一个对象吗?python官方文档中有这样的描述:
每个变量都有标识、类型和值。对象一旦创建,它的标识绝对不会变化;你可以把标识当作该对象在内存中的地址。is运算符用以比较两个对象的标识,内建函数
id()
返回对象标识的整数表示。
```
In [12]: list1 is list2
Out[12]: True
In [13]: list3 is list1
Out[13]: False
In [14]: id(list1)
Out[14]: 4418368144
In [15]: id(list2)
Out[15]: 4418368144
In [16]: id(list3)
Out[16]: 4418128064
```
可以看到`list1 is list2`并且`id(list1) == id(list2)`,这说明list1和list2绑定的是同一个对象,list3则和list1或者list2绑定的是不同的对象,即使是`list3 == list1 == list2`
3. 运算符is
和==
通过上边的例子,有个直观的印象就是
is
用于比较对象的标识,==
用于比较对象的值
通常情况下,我们会更多的使用`==`进行对象间的值的比较,但是在<strong>变量和单例值之间的比较,更应该使用`is`</strong>。这是因为: <strong>`is`运算符比`==`更快,因为它不能重载,这样python不用寻找并调用特殊方法,而是直接比较两个整数ID。`a == b`是语法糖,等同于执行`a.__eq__(b)`。继承自object的`__eq__`方法比较的是两个对象的ID,结果和`is`一样,但是多数内置类型都定义了更有意义的方式,覆盖了`__eq__`,这样就可能会给相等性测试带来更多复杂的处理工作。</strong>
4. 元组的相对不可变性
元组与多数python的集合类型一样(这里的一样指的是:列表、字典、集等而不包括像是str,bytes和array.array这样的单一类型序列,它们保存的不是引用,而是在连续内存中保存的数据本身),保存的是对象的引用。所以,如果引用的元素是可变的,即使元组本身不变,它其中的元素也是可变的。元组的不可变性,指的是它其中保存的引用不变(数据结构和物理内容),与引用的对象无关
5. copy和deepcopy
由上边的例子可以看到,像列表、字典等集合类型,它们内部保存的是对象的引用,所以就会出现这样的情况:
|
|
In [33]和In [34]进行了简单的列表复制,通过内置的构造函数来完成赋值。这个时候l1 == l2
并且l1 is not l2
它们绑定了不同的对象。所以In [36]中l1.append(0)并不会对l2造成影响。
但是由于列表内部保存的是对象的引用,所以l1[1] == [4,5,6]
,如果对l1[1]进行操作,l2自然会收到影响。
In [40]和In [41]分别对l2[1]和l2[2]进行操作,列表在进行+=
操作之后,会就地修改列表,但对于元组来说+=
操作会创建一个新的元组然后重新绑定给变量l2[2],这样l1[2]
和l2[2]
就不是同一个对象。(个人觉得,上边的这个过程可以当作一道优秀的面试题目😊)
对
+=
或者*=
所做的增量复制操作来说,如果操作符左侧绑定的是不可变对象,会创建一个新的对象,如果是可变对象,会就地修改
默认情况下,python进行的复制都是浅复制(副本共享内部对象的引用),通过copy模块提供的deepcopy和copy可以为任何对象做深复制或者浅复制。
```
In [55]: class Bus:
...: def __init__(self,passengers=None):
...: if passengers is None:
...: self.passengers = []
...: else:
...: self.passengers = passengers
...: def pick(self,name):
...: self.passengers.append(name)
...: def drop(self,name):
...: self.passengers.remove(name)
...:
In [56]: import copy
In [57]: bus1 = Bus(['wm','ws','ls','gjy'])
In [58]: bus2 = copy.copy(bus1)
In [59]: bus3 = copy.deepcopy(bus1)
In [60]: map(id,[bus1,bus2,bus3])
Out[60]: [4418132448, 4416805992, 4416805920]
In [61]: bus1.drop('wm')
In [62]: bus2.passengers
Out[62]: ['ws', 'ls', 'gjy']
In [63]: bus3.passengers
Out[63]: ['wm', 'ws', 'ls', 'gjy']
In [64]: map(id,[bus1.passengers,bus2.passengers,bus3.passengers])
Out[64]: [4419499288, 4419499288, 4418048440]
```
可以看到,通过deepcopy,bus3和bus1之间并没有共享内部对象的引用,而通过copy生成的bus2的`passengers`和bus1是相同的对象。
6. 不要使用可变类型作为参数的默认值
通过下边的例子可以证明上边的这条忠告:
|
|
可以看到,在没有定义初始乘客的HauntedBus实例会共享同一个乘客列表,这是因为self.passengers
变成了passengers
参数默认值的别名,默认值在定义函数时计算,因此默认值变成了函数对象的属性,因此,如果默认值是可变对象,并且修改了它的值,那么后续的函数调用都会收到影响。
7. 防御可变参数
如果一个函数接受的是可变参数,那么应该谨慎的考虑是否想要修改传入的参数,是否想要将对这个可变对象的修改作用到函数体之外?
|
|
可以看到Twilightbus可以让乘客莫名其妙的销声匿迹😨。这是因为,在将参数passenger传给Twilightbus的时候,实际上是将self.passengers
变成了passengers的别名,所以每当乘客下车执行drop()
的时候,直接将乘客从列表中抹去。
如果想要避免这种状况,可以在初始化函数中做一些修改:
|
|
经过上边这样的内部处理,就不会发生上边的幽灵巴士案件。所以,在类中直接把参数复制给实例变量以前一定要考虑清楚。
文章作者 rgozi
上次更新 2017-08-20