本章的notebook文件在这里

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

变量

这里首先纠正一个观念, 变量并不是一个存储着值的盒子, 而更像是一个贴在值上的便利贴, 我们假设一个dict存储了关于鲁迅的内容:

1
2
3
4
luxun = {'name': 'zhoushuren', 'penname':'luxun'}
zhoushuren = luxun

luxun == zhoushuren

True
1
id(zhoushuren), id(luxun)

(139695789640392, 139695789640392)

可以看出, 鲁迅和周树人的值是相等的, 且用id函数可以看出二者是同一个对象, 这里体现出luxun,zhoushuren都是别名, 绑定了同一个对象, 对二者之一的修改都会影响到另一个.

1
2
3
4
5
zhoushuren['publication'] = 'The True Story of Ah Q' 
```

```python
luxun
{'name': 'zhoushuren',
 'penname': 'luxun',
 'publication': 'The True Story of Ah Q'}

当然编程中很少用id函数, 通常我们用is运算符来检查标识 (可以理解为内存里的地址)是否一致.
需要注意的是, 往往我们更专注两个变量的值是否一致, 因此==出现的次数比is多得多.

元组的相对不可变性

我们之前提到过, 元组赋值后是不可变的, 但是如果其中的元素有list, 该list是可变的.
此外我们可以通过+=操作来更改元组, 但是本质上它不是改变元组的大小而是将原来的对象删除, 赋值给一个新的对象.
如下可以观察到+=操作前后, 元组a的标识发生了改变.

1
2
a = (1,2,3)
id(a)
139695789497656
1
a += (4,5)
1
a
(1, 2, 3, 4, 5)
1
id(a) 
139695789516672

复制

在python中, 对列表等对象的复制默认是做浅复制, 即仅复制最外层容器, 副本中的元素是源容器中的引用.
此时如果所有元素都是不可变的,那么往往不会出问题, 但是如果有可变元素,就会有问题了.

1
2
3
l1 = [3, [1,2,3], (4,5,6)]  
l2 = list(l1)
l2
[3, [1, 2, 3], (4, 5, 6)]
1
2
l1.append(100)
print('l1', l1,'\nl2', l2)
l1 [3, [1, 2, 3], (4, 5, 6), 100] 
l2 [3, [1, 2, 3], (4, 5, 6)]
1
2
l1[1].remove(2)
print('l1', l1,'\nl2', l2)
l1 [3, [1, 3], (4, 5, 6), 100] 
l2 [3, [1, 3], (4, 5, 6)]
1
2
3
l2[1] += [33, 22]
l2[2] += (30, 20)
print('l1', l1,'\nl2', l2)
l1 [3, [1, 3, 33, 22], (4, 5, 6), 100] 
l2 [3, [1, 3, 33, 22], (4, 5, 6, 30, 20)]

从上面可以看出, 我们对l1的最外层容器做修改(append(100))并不会影响到l2, 但是用于是做浅复制, 因此容器中可变元素这里也就是初始为[1,2,3]的列表是同一个引用. 因此在l1中修改该列表(remove(2), +=[33,22])也会影响到l2.

需要注意, 这里的对tuple操作显示出l2l1中的元组不是一个对象.

有时我们需要对任意对象做深度复制,即所有元素都不共享内部对象的引用而是复制其值. 此时我们需要用到copy模块的deepcopy函数:

1
2
3
4
from copy import deepcopy
l1 = [3, [1,2,3], (4,5,6)]
l2 = deepcopy(l1)
print('l1', l1,'\nl2', l2)
l1 [3, [1, 2, 3], (4, 5, 6)] 
l2 [3, [1, 2, 3], (4, 5, 6)]
1
2
l1[1].remove(2)
print('l1', l1,'\nl2', l2)
l1 [3, [1, 3], (4, 5, 6)] 
l2 [3, [1, 2, 3], (4, 5, 6)]

函数参数引用

python只支持共享传参, 即函数的各个形式参数获得实参中各个引用的副本, 也就是说形参是实参的别名.
那么当函数收到可变对象作为参数时,它可能会修改该对象, 引发意外的后果.

如下:

1
2
3
4
5
def change(a):
a.append('haha')

ps = ['keke', 'hehe']
ps
['keke', 'hehe']
1
2
change(ps)
ps
['keke', 'hehe', 'haha']

由此,我们得到的教训就是:

  • 不要用可变类型作为函数的默认值, 例如不要使用空列表[]作为函数默认值而使用None
  • 谨慎考虑调用方是否期望改变传入的参数

del和垃圾回收

和直觉不同的是, python中的del并不会直接删除对象, 它只是删除一个名称. 当del删除一个名称时, 该名称绑定的对象的引用计数就会减一, 当引用计数为零时, python的垃圾回收会自动删除该对象.

除了我们常见的引用, 还有一类特殊的弱引用, weakref.ref, 这类引用不会增加对象的引用计数, 因此常被用于缓存应用中.