对象引用、可变性和垃圾回收
- 本章的主题是对象与对象名称之间的区别。
名称不是对象,而是单独的东西。
- 本章先以一个比喻说明 Python 的变量:
变量是标注,而不是盒子。
如果你不知道引用式变量是什么,可以像这样对别人解释别名。 - 本章讨论对象标识、值和别名等概念。随后,本章会揭露元组的一个神奇特性:元组是不可变的,但是其中的值可以改变,之后就引申到浅复制和深复制。接下来的话题是引用和函数参数:可变的参数默认值导致的问题,以及如何安全地处理函数的调用者传入的可变参数。
- 本章最后一节讨论垃圾回收、del 命令,以及如何使用弱引用“记住”对象,而无需对象本身存在。
- 本章的内容有点儿枯燥,但是这些话题却是解决 Python 程序中很多不易察觉的 bug 的关键。
- 首先,我们要
抛弃变量是存储数据的盒子这一错误观念。
1. 变量不是盒子
- Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为
附加在对象上的标注。
- 如下示例所示的交互式控制台中,无法使用“变量是盒子”做解释。图说明了在 Python 中为什么不能使用盒子比喻,而便利贴则指出了变量的正确工作方式。
'''
变量 a 和 b 引用同一个列表,而不是那个列表的副本
'''
if __name__ == '__main__':
a = [1, 2, 3]
b = a
a.append(4)
print(b)
# [1, 2, 3, 4]
- 如果把变量想象为盒子,那么无法解释 Python 中的赋值;应该把变量视作便利贴,这样示例中的行为就好解释了。
对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了。
示例 证明赋值语句的右边先执行:
'''
创建对象之后才会把变量分配给对象
'''
class Gizmo:
def __init__(self):
print('Gizmo id: %d' % id(self))
if __name__ == '__main__':
x = Gizmo()
# Gizmo id: 2915825302496
try:
y = Gizmo() * 10
# Gizmo id: 2915825505280
except TypeError as e:
print(e)
# unsupported operand type(s) for *: 'Gizmo' and 'int'
print(dir())
# ['Gizmo', '__annotations__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'x']
- 示例说明在乘法运算中使用 Gizmo 实例会抛出异常,在尝试求积之前其实会创建一个新的 Gizmo 实例。不会创建变量 y,因为在对赋值语句的右边进行求值时抛出了异常。
- 为了理解 Python 中的赋值语句,应该始终先读右边。
对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注
。忘掉盒子吧! - 因为变量只不过是标注,所以无法阻止为对象贴上多个标注。贴的多个标注,就是别名。参见下一节。
2. 标识、相等性和别名
- Lewis Carroll 是 Charles Lutwidge Dodgson 教授的笔名。Carroll 先生指的就是 Dodgson 教授,二者是同一个人。示例用 Python 表达了这个概念:
'''
charles 和 lewis 指代同一个对象
'''
if __name__ == '__main__':
charles = {
'name': 'Charles L. Dodgson', 'born': 1832}
lewis = charles
print(lewis is charles)
# True
print(id(charles), id(lewis))
# 2186528121728 2186528121728
lewis['balance'] = 950
print(charles)
# {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
- lewis 是 charles 的别名,is 运算符和 id 函数确认了这一点。向 lewis 中添加一个元素相当于向 charles 中添加一个元素。
- 然而,假如有冒充者(姑且叫他 Alexander Pedachenko 博士)生于 1832年,声称他是 Charles L. Dodgson。这个冒充者的证件可能一样,但是Pedachenko 博士不是 Dodgson 教授。这种情况如下所示:
alex = {
'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
print(alex == charles)
# True
print(alex is charles)
# False
- alex 指代的对象与赋值给 charles 的对象内容一样。比较两个对象,结果相等,这是因为 dict 类的
__eq__
方法就是这样实现的。但它们是不同的对象。这是 Python 说明标识不同的方式:a is not b
。 - lewis 和 charles 是别名,即两个变量绑定同一个对象。而 alex 不是 charles 的别名,因为二者绑定的是不同的对象。alex 和 charles 绑定的对象具有相同的值(== 比较的就是值),但是它们的标识不同。
- 每个变量都有标识、类型和值。
对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。is 运算符比较两个对象的标识;id() 函数返回对象标识的整数表示。
- 其实,实际编程中很少使用 id() 函数。标识最常使用 is 运算符检查,而不是直接比较 ID。接下来讨论 is 和 == 的异同。
2.1 在 == 和 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 一样。但是多数内置类型使用更有意义的方式覆盖了__eq__
方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型集合或嵌套层级深的结构时。 -
在结束对标识和相等性的讨论之前,我们来看看著名的不可变类型 tuple(元组),它没有你想象的那么一成不变。
2.2 元组的相对不可变性
- 元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。 如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,
元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
- 元组的值会随着引用的可变对象的变化而变。元组中不可变的是元素的标识。如下示例:
if __name__ == '__main__':
t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
print(t1 == t2)
# True
print(t1[-1])
# [30, 40]
print(id(t1[-1]))
# 1907465332864
t1[-1].append(99)
print(id(t1[-1]))
# 1907465332864
print(t1 == t2)
# False
- 示例说明 t1 不可变,但是 t1[-1] 可变。虽然 t1