对象引用、可变性和垃圾回收

名称不是对象,而是单独的东西。

Python 的变量:变量是标注,而不是盒子。

Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为附加在对象上的标注

对引用式变量来说,说把变量分配给对向更合理

Python 中的赋值语句,始终先读右边,对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。

每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址。is 运算符比较两个对象的标识;id() 函数返回对象标识的整数表示。ID 一定是唯一的数值标注,而且在对象的生命周期中绝不会变。

1
2
3
4
5
alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
alex == charles
>>> True
alex is not charles
True

在==和is之间选择
== 运算符比较两个对象的值(对象中保存的数据),而 is 比较对象的标识(id号)。

元组的相对不可变性

  • 元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。

  • 即便元组本身不可变,元素依然可变

  • 元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关
  • str、bytes 和 array.array 等单一类型序列是扁平的,它们保存的不是引用,而是在连续的内存中保存数据本身(字符、字节和数字)
1
2
3
4
5
6
7
8
9
10
11
12
13
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

默认做浅复制

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法

对列表和其他可变序列来说,还能使用简洁的 l2 = l1[:]语句创建副本

1
2
3
4
5
6
7
8
>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1)
>>> l2
>>> [3, [55, 44], (7, 8, 9)]
>>> l2 == l1
>>> True
>>> l2 is l1
>>> False

然而,构造方法或 [:] 做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)

如果所有元素都是不可变的,那么这样没有问题,还能节省内存

但是,如果有可变的元素,可能就会导致意想不到的问题

1
2
3
4
5
6
7
8
9
10
l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1) # l2 是 l1 的浅复制副本
l1.append(100) # 把 100 追加到 l1 中,对 l2 没有影响。
l1[1].remove(55) # 把内部列表 l1[1] 中的 55 删除。这对 l2 有影响,因为l2[1] 绑定的列表与 l1[1]是同一个。
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22] # 对可变的对象来说,如 l2[1] 引用的列表,+= 运算符就地修改列表。这次修改在l1[1] 中也有体现,因为它是 l2[1] 的别名。
l2[2] += (10, 11) # 对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量 l2[2]。这等同于l2[2] = l2[2] + (10, 11)。现在,l1 和 l2 中最后位置上的元组不是同一个对象。
print('l1:', l1)
print('l2:', l2)

对元组来说,+= 运算符创建一个新元组

为任意对象做深复制和浅复制

copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> import copy
>>> bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
>>> bus2 = copy.copy(bus1)
>>> bus3 = copy.deepcopy(bus1)
>>> id(bus1), id(bus2), id(bus3)
(4301498296, 4301499416, 4301499752) # 使用 copy 和 deepcopy,创建 3 个不同的 Bus 实例。
>>> bus1.drop('Bill')
>>> bus2.passengers
['Alice', 'Claire', 'David'] # bus1 中的 'Bill' 下车后,bus2 中也没有他了。
>>> id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)
(4302658568, 4302658568, 4302657800) # 审查 passengers 属性后发现,bus1 和 bus2 共享同一个列表对象,因为 bus2 是bus1 的浅复制副本。
>>> bus3.passengers
['Alice', 'Bill', 'Claire', 'David'] # bus3 是 bus1 的深复制副本,因此它的 passengers 属性指代另一个列表。

一般来说,深复制不是件简单的事。如果对象有循环引用,那么这个朴素的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用

函数的参数作为引用时

  • Python 唯一支持的参数传递模式是共享传参(call by sharing)

  • 共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。

  • 函数可能会修改作为参数传入的可变对象(比如列表),但是无法修改那些对象的
    标识(即不能把一个对象替换成另一个对象)

不要使用可变类型作为参数的默认值

默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数

del和垃圾回收

  • del 语句删除名称,而不是对象

  • del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时

  • 重新绑定也可能会导致对象的引用数量归零,导致对象被销毁

  • 有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。
  • 弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)
  • 弱引用不会妨碍所指对象被当作垃圾回收。
  • 弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回 None
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) # 创建弱引用对象 wref,下一行审查它。
>>> wref
<weakref at 0x100637598; to 'set' at 0x100636748>
>>> wref() # 调用 wref() 返回的是被引用的对象,{0, 1}。因为这是控制台会话,所以 {0, 1}会绑定给 _ 变量。
{0, 1}
>>> a_set = {2, 3, 4} # a_set 不再指代 {0, 1} 集合,因此集合的引用数量减少了。但是 _ 变量仍然指代它。
>>> wref() # 调用 wref() 依旧返回 {0, 1}。
{0, 1}
>>> wref() is None # 计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。但是,随后 _ 绑定到结果值 False。现在 {0, 1} 没有强引用了。
False
>>> wref() is None # 因为 {0, 1} 对象不存在了,所以 wref() 返回 None。
True
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import weakref
>>> stock = weakref.WeakValueDictionary() # stock 是 WeakValueDictionary 实例。
>>> catalog = [Cheese('Red Leicester'), Cheese('Tilsit'),
... Cheese('Brie'), Cheese('Parmesan')]
...
>>> for cheese in catalog:
... stock[cheese.kind] = cheese # stock 把奶酪的名称映射到 catalog 中 Cheese 实例的弱引用上。
...
>>> sorted(stock.keys())
['Brie', 'Parmesan', 'Red Leicester', 'Tilsit'] # stock 是完整的。
>>> del catalog
>>> sorted(stock.keys())
['Parmesan'] # 删除 catalog 之后,stock 中的大多数奶酪都不见了,这是 WeakValueDictionary的预期行为。为什么不是全部呢?
>>> del cheese
>>> sorted(stock.keys())
[]

临时变量引用了对象,这可能会导致该变量的存在时间比预期长。通常,这对局部变量来说不是问题,因为它们在函数返回时会被销毁。

不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict*实例不能作为所指对象,但是它们的子类可以轻松地解决这个问题

set 实例可以作为所指对象

变量保存的是引用,这一点对 Python 编程有很多实际的影响

  • 简单的赋值不创建副本。
  • 对 += 或 *= 所做的增量赋值来说,如果左边的变量绑定的是不可变对象,会创建新对象;如果是可变对象,会就地修改。
  • 为现有的变量赋予新值,不会修改之前绑定的变量。这叫重新绑定:现在变量绑定了其他对象。如果变量是之前那个对象的最后一个引用,对象会被当作垃圾回收。
  • 函数的参数以别名的形式传递,这意味着,函数可能会修改通过参数传入的可变对象。这一行为无法避免,除非在本地创建副本,或者使用不可变对象(例如,传入元组,而不传入列表)。
  • 使用可变类型作为函数参数的默认值有危险,因为如果就地修改了参数,默认值也就变了,这会影响以后使用默认值的调用。

可以在自己的类中定义 eq 方法,决定 == 如何比较实例。如果不覆盖 eq 方法,那么从 object 继承的方法比较对象的 ID