Python拾遗 对象引用 --流畅的python
本章的notebook文件在这里
对象引用, 可变性和垃圾回收
变量
这里首先纠正一个观念, 变量并不是一个存储着值的盒子, 而更像是一个贴在值上的便利贴, 我们假设一个dict存储了关于鲁迅的内容:1
2
3
4luxun = {'name': 'zhoushuren', 'penname':'luxun'}
zhoushuren = luxun
luxun == zhoushuren
True1
id(zhoushuren), id(luxun)
(139695789640392, 139695789640392)
可以看出, 鲁迅和周树人的值是相等的, 且用id
函数可以看出二者是同一个对象, 这里体现出luxun
,zhoushuren
都是别名, 绑定了同一个对象, 对二者之一的修改都会影响到另一个.
1 | zhoushuren['publication'] = 'The True Story of Ah Q' |
{'name': 'zhoushuren',
'penname': 'luxun',
'publication': 'The True Story of Ah Q'}
当然编程中很少用id
函数, 通常我们用is
运算符来检查标识 (可以理解为内存里的地址)是否一致.
需要注意的是, 往往我们更专注两个变量的值是否一致, 因此==
出现的次数比is
多得多.
元组的相对不可变性
我们之前提到过, 元组赋值后是不可变的, 但是如果其中的元素有list, 该list
是可变的.
此外我们可以通过+=
操作来更改元组, 但是本质上它不是改变元组的大小而是将原来的对象删除, 赋值给一个新的对象.
如下可以观察到+=
操作前后, 元组a
的标识发生了改变.
1 | a = (1,2,3) |
139695789497656
1 | a += (4,5) |
1 | a |
(1, 2, 3, 4, 5)
1 | id(a) |
139695789516672
复制
在python中, 对列表等对象的复制默认是做浅复制, 即仅复制最外层容器, 副本中的元素是源容器中的引用.
此时如果所有元素都是不可变的,那么往往不会出问题, 但是如果有可变元素,就会有问题了.
1 | l1 = [3, [1,2,3], (4,5,6)] |
[3, [1, 2, 3], (4, 5, 6)]
1 | l1.append(100) |
l1 [3, [1, 2, 3], (4, 5, 6), 100]
l2 [3, [1, 2, 3], (4, 5, 6)]
1 | l1[1].remove(2) |
l1 [3, [1, 3], (4, 5, 6), 100]
l2 [3, [1, 3], (4, 5, 6)]
1 | l2[1] += [33, 22] |
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
操作显示出l2
和l1
中的元组不是一个对象.
有时我们需要对任意对象做深度复制,即所有元素都不共享内部对象的引用而是复制其值. 此时我们需要用到copy
模块的deepcopy
函数:
1 | from copy import deepcopy |
l1 [3, [1, 2, 3], (4, 5, 6)]
l2 [3, [1, 2, 3], (4, 5, 6)]
1 | l1[1].remove(2) |
l1 [3, [1, 3], (4, 5, 6)]
l2 [3, [1, 2, 3], (4, 5, 6)]
函数参数引用
python只支持共享传参, 即函数的各个形式参数获得实参中各个引用的副本, 也就是说形参是实参的别名.
那么当函数收到可变对象作为参数时,它可能会修改该对象, 引发意外的后果.
如下:
1 | def change(a): |
['keke', 'hehe']
1 | change(ps) |
['keke', 'hehe', 'haha']
由此,我们得到的教训就是:
- 不要用可变类型作为函数的默认值, 例如不要使用空列表
[]
作为函数默认值而使用None
- 谨慎考虑调用方是否期望改变传入的参数
del和垃圾回收
和直觉不同的是, python中的del并不会直接删除对象, 它只是删除一个名称. 当del删除一个名称时, 该名称绑定的对象的引用计数就会减一, 当引用计数为零时, python的垃圾回收会自动删除该对象.
除了我们常见的引用, 还有一类特殊的弱引用, weakref.ref
, 这类引用不会增加对象的引用计数, 因此常被用于缓存应用中.