对象引用和可变性
变量不是盒子
人们经常使用“变量是盒子”这样的比喻,但是这有碍于理解面向对象语言中的引用式变量。
Python变量类似于Java中的引用式变量,因此最好把它们理解为附加在对象上的标注。
#示例1:变量a和b引用同一个列表,而不是那个列表的副本
>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]
如果把变量想成一个盒子,那么无法解释python中的赋值,应该把变量视作便利贴,这样示例1中的行为就能得到解释了。
对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了。比如:讲到seesaw对象时,会说“把变量s分配给seesaw”,绝不会说“把seesaw分配给变量s”。
下面示例2证明赋值语句的右边先执行。
创建对象之后才会把变量分配给对象
>>> class Gizmo:
... def __init__(self):
... print('Gizmo id: %d' % id(self))
...
>>> x = Gizmo()
Gizmo id: 4301489152 #➊
>>> y = Gizmo() * 10 #➋
Gizmo id: 4301489432 #➌
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
>>>
>>> dir() #➍
['Gizmo', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']
➊ 输出的Gizmo id: …是创建Gizmo实例的副作用。
➋ 在乘法运算中使用Gizmo实例会抛出异常。
➌ 这里表明,在尝试求积之前其实会创建一个新的Gizmo实例。
➍ 但是,肯定不会创建变量y,因为在对赋值语句的右边进行求值时抛出了异常。
为了理解Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。
标识、相等性和别名
#示例3:charles和lewis指代同一个对象
>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832}
>>> lewis = charles #➊
>>> lewis is charles
True
>>> id(charles), id(lewis) #➋
(3024388455088, 3024388455088)
>>> lewis['balance'] = 950 #➌
>>> charles
{'name': 'Charles L. Dodgson', 'balance': 950, 'born': 1832}
➊ lewis是charles的别名。
➋ is运算符和id函数确认了这一点。
➌ 向lewis中添加一个元素相当于向charles中添加一个元素。
#示例4:alex与charles比较的结果是相等,但alex不是charles
>>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} #➊
>>> alex == charles #➋
True
>>> alex is not charles #➌
True
>>>id(charles),id(alex)
(3024388455088, 3024387755464)
➊ alex指代的对象与赋值给charles的对象内容一样。
➋ 比较两个对象,结果相等,这是因为dict类的__eq__方法就是这样实现的。
➌ 但它们是不同的对象。这是Python说明标识不同的方式:a is not b。
charles和alex绑定同一个对象,alex绑定另一个具有相同内容的对象。
示例2中lewis和charles是别名,即两个变量绑定同一个对象。而alex不是charles的别名,因为二者绑定的是不同的对象。alex和charles绑定的对象具有相同的值(==比较的就是值),但是它们的标识不同。
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。is运算符比较两个对象的标识;id()函数返回对象标识的整数表示。
对象ID的真正意义在不同的实现中有所不同。在CPython中,id()返回对象的内存地址,但是在其他Python解释器中可能是别的值。关键是,ID一定是唯一的数值标注,而且在对象的生命周期中绝不会变。
其实,编程中很少使用id()函数。标识最常使用is运算符检查,而不是直接比较ID。
在==和is之间选择
==运算符比较两个对象的值(对象中保存的数据),而is比较对象的标识。通常,我们关注的是值,而不是标识,因此Python代码中==出现的频率比is高。
然而,在变量和单例值之间比较时,应该使用is。目前,最常使用is检查变量绑定的值是不是None。
下面是推荐的写法:
x is None
否定的写法:
x is not None
is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a == b是语法糖,等同于a.eq(b)。继承自object的__eq__方法比较两个对象的ID,结果与is一样.
元组的相对可变性
元组与多数Python集合(列表、字典、集,等等)一样,保存的是对象的引用。1如果引用的元素是可变的,即便元组本身不可变,元素依然可变。
也就是说,元组的不可变性其实是指tuple数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
下面的示例5中元组的值会随着引用的可变对象的变化而变。元组中不可变的是元素的标识。
#示例5:一开始,t1和t2相等,但是修改t1中的一个可变元素后,二者不相等了
>>> t1 = (1, 2, [30, 40]) #➊
>>> t2 = (1, 2, [30, 40]) #➋
>>> t1 == t2 #➌
True
>>> id(t1[-1]) #➍
4302515784
>>> t1[-1].append(99) #➎
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1]) #➏
4302515784
>>> t1 == t2 #➐
False
➊ t1不可变,但是t1[-1]可变。
➋ 构建元组t2,它的元素与t1一样。
➌ 虽然t1和t2是不同的对象,但是二者相等——与预期相符。
➍ 查看t1[-1]列表的标识。
➎ 就地修改t1[-1]列表。
➏ t1[-1]的标识没变,只是值变了。
➐ 现在,t1和t2不相等。
复制对象时,相等性和标识之间的区别有更深入的影响。副本与源对象相等,但是ID不同。