对象引用、可变性和垃圾回收
此笔记记录于《流畅的 python》,大部分为其中的摘要,少部分为笔者自己的理解;笔记为 jupyter 转的 markdown,原始版 jupyter 笔记在这个仓库
本章的主题是对象与对象名称之间的区别。名称不是对象,而是单独的东西。
变量是标注,而不是盒子。
变量不是盒子
a = [1, 2, 3]
b = a
a.append(4)
b
[1, 2, 3, 4]
a = 1
b = a # 原始值是会被复制的,这个和 js 差不多
a = 2
b
1
对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了。
标识、相等性和别名
# charles 和 lewis 指代同一个对象
charles = {'name': 'Charles L. Dodgson', 'born': 1832}
lewis = charles
lewis is charles
True
id(charles), id(lewis)
(2750850954752, 2750850954752)
lewis['balance'] = 950
charles
{'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
# 假如有一个冒充者,属性值都是相等的,但是并不是一个对象
alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950}
alex == charles
True
alex is not charles
True
上述比较中也顺便说明了==
操作和is not
操作之间的区别:
==
运算符比较两个对象的值(对象中保存的数据)- 而
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__
方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型集合或嵌套层级深的结构时。
元组的相对不可变性
元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。如果引用的元素是可变的,即便元组本身不可变,元素依然可变。
默认做浅复制
复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法
l1 = [3, [55, 44], (7, 8, 9)]
l2 = list(l1)
l2
[3, [55, 44], (7, 8, 9)]
l2 == l1
True
l2 is l1
False
构造方法或[:]
做的是浅复制(即复制了最外层容器,副本中的元素是源容器中元素的引用)。如果所有元素都是不可变的,那么这样没有问题,还能节省内存。但是,如果有可变的元素,可能就会导致意想不到的问题--修改深层嵌套的可变元素可能会影响到副本。
为任意对象做深复制和浅复制:copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。
函数的参数作为引用时
Python 唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言都采用这一模式
共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。这种方案的结果是,函数可能会修改作为参数传入的可变对象,但是无法修改那些对象的标识(即不能把一个对象替换成另一个对象)。
不要使用可变类型作为参数的默认值,否则,你的默认值可能一直在变化之中。
del 与垃圾回收
对象绝不会自行销毁;然而,无法得到对象时,可能会被当作垃圾回收。
del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时。重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。
弱引用
正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。
弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
import weakref
a_set = {0, 1}
wref = weakref.ref(a_set)
wref
<weakref at 0x000002807B80CEF0; to 'set' at 0x000002807B58A5E0>
a_set = {2, 3, 4} # 这个时候 a_set 指向了一个新的对象
wref
<weakref at 0x000002807B80CEF0; dead>
wref() is None
True
WeakValueDictionary 简介
WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的键会自动从 WeakValueDictionary 中删除。因此,WeakValueDictionary 经常用于缓存。
弱引用的局限性:
不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本的 list 和 dict 实例不能作为所指对象,但是,它们的子类可以轻松地解决这个问题。
python 对不可变类型施加的把戏
我惊讶地发现,对元组 t 来说,t[:]
不创建副本,而是返回同一个对象的引用。此外,tuple(t)
获得的也是同一个元组的引用。
t1 = (1, 2, 3)
t2 = tuple(t1)
t2 is t1
True
t3 = t1[:]
t3 is t1
True
str、bytes 和 frozenset 实例也有这种行为。注意,frozenset 实例不是序列,因此不能使用fs[:]
(fs 是一个 frozenset 实例)。但是,fs.copy( )
具有相同的效果:它会欺骗你,返回同一个对象的引用,而不是创建一个副本