Skip to content

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

此笔记记录于《流畅的python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为jupyter转的markdown,原始版jupyter笔记在这个仓库

本章的主题是对象与对象名称之间的区别。名称不是对象,而是单独的东西。

变量是标注,而不是盒子。

变量不是盒子

python
a = [1, 2, 3]
b = a
a.append(4)
b
[1, 2, 3, 4]
python
a = 1 
b = a # 原始值是会被复制的,这个和js差不多
a = 2
b
1

对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了。

标识、相等性和别名

python
# charles和lewis指代同一个对象
charles = {'name': 'Charles L. Dodgson', 'born': 1832}
lewis = charles
lewis is charles
True
python
id(charles), id(lewis)
(2750850954752, 2750850954752)
python
lewis['balance'] = 950
charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
python
# 假如有一个冒充者,属性值都是相等的,但是并不是一个对象
alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
alex == charles
True
python
alex is not charles
True

上述比较中也顺便说明了==操作和is not操作之间的区别:

  • ==运算符比较两个对象的值(对象中保存的数据)
  • is比较对象的标识。

通常,我们关注的是值,而不是标识,因此Python代码中==出现的频率比is

然而,在变量和单例值之间比较时,应该使用is。目前,最常使用is检查变量绑定的值是不是None。下面是推荐的写法:

python
x is None
x is not None

is运算符比==速度快,因为它不能重载,所以Python不用寻找并调用特殊方法,而是直接比较两个整数ID。而a==b是语法糖,等同于a.__eq__(b)。继承自object的__eq__方法比较两个对象的ID,结果与is一样。但是多数内置类型使用更有意义的方式覆盖了__eq__方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型集合或嵌套层级深的结构时。

元组的相对不可变性

元组与多数Python集合(列表、字典、集,等等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。

默认做浅复制

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

python
l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1)
l2
[3, [55, 44], (7, 8, 9)]
python
l2 == l1
True
python
l2 is l1
False

构造方法或[:]做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题--修改深层嵌套的可变元素可能会影响到副本。

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

函数的参数作为引用时

Python唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言都采用这一模式

共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。

不要使用可变类型作为参数的默认值,否则,你的默认值可能一直在变化之中。

del与垃圾回收

对象绝不会自行销毁;然而,无法得到对象时,可能会被当作垃圾回收。

del语句删除名称,而不是对象。del命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。

弱引用

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。

弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。

python
import weakref

a_set = {0, 1}
wref = weakref.ref(a_set)
wref
<weakref at 0x000002807B80CEF0; to 'set' at 0x000002807B58A5E0>
python
a_set = {2, 3, 4} # 这个时候a_set指向了一个新的对象
wref
<weakref at 0x000002807B80CEF0; dead>
python
wref() is None
True

WeakValueDictionary简介

WeakValueDictionary类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从WeakValueDictionary中删除。因此,WeakValueDictionary经常用于缓存。

弱引用的局限性

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

python对不可变类型施加的把戏

我惊讶地发现,对元组t来说,t[:]不创建副本,而是返回同一个对象的引用。此外,tuple(t)获得的也是同一个元组的引用。

python
t1 = (1, 2, 3)
t2 = tuple(t1)
t2 is t1
True
python
t3 = t1[:]
t3 is t1
True

str、bytes和frozenset实例也有这种行为。注意,frozenset实例不是序列,因此不能使用fs[:](fs是一个frozenset实例)。但是,fs.copy( )具有相同的效果:它会欺骗你,返回同一个对象的引用,而不是创建一个副本