《Fluent Python》CH.08_面向对象惯用_对象引用、可变性和垃圾回收 (深浅复制、幽灵入参、引用计数法、弱引用缓冲池、不可变类型可能存在的缓冲池问题)
主要内容
- 本章讨论对象标识、值和别名等概念
- 是引用和函数参数:可变的参数默认 值导致的问题,以及如何安全地处理函数的调用者传入的可变参数。
- 最后一节讨论垃圾回收、del 命令,以及如何使用弱引用“记住”对 象,而无需对象本身存在
内容补充
- 元组的相对不可变性 (自身和内部的引用id标示始终不会改变,拒绝直接下标赋值操作,可以使用内部的add另一个元组)
- 在函数中,python直接使用'='来接传入的实参,即形参使用‘=’得到了实参的引用;故在函数内部不会更改原对象的内存地址,值倒是可以变更的。
- 基本数据类型和元组,属于不可变类型,传参时不受影响。
其他
-
转换命令: jupyter nbconvert --to markdown E:\PycharmProjects\TianChiProject\00_山枫叶纷飞\competitions\013_fluent_python\CH.08_面向对象_对象引用、可变性和垃圾回收.ipynb
-
总页数: 378-345=35页
8.1 变量不是盒子
示例 8-1 变量 a 和 b 引用同一个列表,而不是那个列表的副本
?示例 8-1.png
a = [1,2,3]
b=a
a.append(4)
b
[1, 2, 3, 4]
每个变量都有标识、类型和值。对象一旦创建,它的标识绝不会 变;你可以把标识理解为对象在内存中的地址。is 运算符比较两个 对象的标识;id() 函数返回对象标识的整数表示。
对象 ID 的真正意义在不同的实现中有所不同。在 CPython 中,id() 返 回对象的内存地址,但是在其他 Python 解释器中可能是别的值。关键 是,ID 一定是唯一的数值标注,而且在对象的生命周期中绝不会变。
其实,编程中很少使用 id() 函数。标识最常使用 is 运算符检查,而 不是直接比较 ID。
接下来讨论 is 和 == 的异同。
8.2.1 在==和is之间选择
- is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用 特殊方法,而是直接比较两个整数 ID。
- 而 a == b 是语法糖,等同于 a.eq(b)。继承自 object 的 eq 方法比较两个对象的 ID,结 果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了 eq 方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型集合或嵌套层级深的结构时。
在变量和单例值之间比较时,应该使用 is。目前,最常使用 is 检查变量绑定的值是不是 None。下面是推荐的写法:
x = 233
x is None
False
x is not None
True
8.2.2 元组的相对不可变性 (自身和内部的引用标示不会改变,不支持直接赋值操作)
t2 = (1, 2, [30, 40])
t1 = (1, 2, [30, 40])
t3 = t2.__add__(t1)
t3
(1, 2, [30, 40], 1, 2, [30, 40])
print(id(t1))
print(id(t2))
print(id(t3))
1899881884336
1899881884192
1899883188968
print('尝试改变t1的内部的值引用的数值: (TypeError: \'tuple\' object does not support item assignment)')
t1[1] = 2333
尝试改变t1的内部值:
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-12-c08852b11aba> in <module>
1 print('尝试改变t1的内部值: ')
----> 2 t1[1] = 2333
3 id(t1)
4
TypeError: 'tuple' object does not support item assignment
print(id(t1[2]))
t1[2].append(233)
print('此时元组的引用的标示未发生变化: ', id(t1))
print(id(t1[2]))
1899887645704
此时元组的引用的标示未发生变化: 1899881884336
1899887645704
lst = [1,2,3]
print(id(lst))
lst.append(233)
print(id(lst))
1899887548872
1899887548872
8.3 默认做浅复制copy.copy(),使用copy.deepcopy()实现深copy
引用
- 使用=
浅复制 - 使用copy.copy() (更改了复制对象的地址,但内部地址引用没有发生变化)
深复制 - 循环复制内部对象,还要考虑相互引用的死循环问题
- 使用copy.deepcopy
示例 8-9 使用 copy 和 deepcopy 产生的影响
import copy
obj = [[1,2,3,4], (1,3,3), [[2333]]]
obj2 = obj
obj2_1 = copy.copy(obj)
obj3 = copy.deepcopy(obj)
id(obj), id(obj2), id(obj2_1), id(obj3)
# (1899888691272, 1899888691272,
#
# 1899888485256, 1899888723912)
(1899888691272, 1899888691272, 1899888485256, 1899888723912)
易得, 直接=的对象内存地址一致.
再看一下复制的obj的第一个元素的id:
id(obj[0]), id(obj2[0]), \
id(obj2_1[0]), id(obj3[0])
(1899888577160, 1899888577160, 1899888577160, 1899888724552)
易得,前三个的复制对象的内部引用未发生变化;第四个为深拷贝,内部的对象相当于重新new了一遍。
示例 8-10 循环引用:b 引用 a,然后追加到 a 中;deepcopy 会 想办法复制 a
a = [10, 20]
b = [a, 30]
a.append(b)
a
[10, 20, [[...], 30]]
from copy import deepcopy
c = deepcopy(b)
c
[[10, 20, [...]], 30]
实现特殊方法 copy() 和 deepcopy()
此外,深复制有时可能太深了。例如,对象可能会引用不该复制的外部 资源或单例值。我们可以实现特殊方法 copy() 和 deepcopy(),控制 copy 和 deepcopy 的行为,详情参见 copy 模 块的文档(http://docs.python.org/3/library/copy.html)。
8.4 函数的参数作为引用时
Python 唯一支持的参数传递模式是共享传参(call by sharing)。
多数面 向对象语言都采用这一模式,包括 Ruby、Smalltalk 和 Java(Java 的引用类型是这样,基本类型按值传参——则不一样),但在细节上还是有区别。
总之,python的形参特点总结如下:
- 在函数中,python直接使用'='来接传入的实参,即形参使用‘=’得到了实参的引用;
- 故在函数内部不会更改原对象的内存地址,值倒是可以变更的;
- 基本数据类型和元组,属于不可变类型,传参时不受影响。
8.4.1 不要使用可变类型作为参数的默认值
可选参数可以有默认值,这是 Python 函数定义的一个很棒的特性,这样 我们的 API 在进化的同时能保证向后兼容。然而,我们应该避免使用可 变的对象作为参数的默认值。
示例 8-12 一个简单的类,说明可变默认值的危险
class HauntedBus:
"""备受幽灵乘客折磨的校车"""
def __init__(self, passengers=[]):
self.passengers = passengers
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name)
示例 8-13 备受幽灵乘客折磨的校车
# 举例bus1和bus2都使用空的构造器
bus1 = HauntedBus()
bus1.pick('Charlie')
bus1.passengers
['Charlie']
bus2 = HauntedBus()
bus2.passengers
['Charlie']
print(id(bus1.passengers))
print(id(bus2.passengers))
1899891593544
1899891593544
简单总结
问题在于,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列表。
实例化 HauntedBus 时,如果 传入乘客,会按预期运作。但是不为 HauntedBus 指定乘客的话,奇怪 的事就发生了,这是因为 self.passengers 变成了passengers 参数 默认值的别名。
出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。
因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。
8.4.2 防御可变参数
如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。
示例 8-14 从 TwilightBus 下车后,乘客消失了
basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = HauntedBus(basketball_team)
bus.drop('Tina')
bus.passengers
['Sue', 'Maya', 'Diana', 'Pat']
basketball_team
['Sue', 'Maya', 'Diana', 'Pat']
小结
- TwilightBus 违反了设计接口的最佳实践,即“最少惊讶原则”。学生从 校车中下车后,她的名字就从篮球队的名单中消失了,这确实让人惊讶。
- 后续处理的原则,设置passengers入参为None,或者值声明入参形式,在内部接住参数的时候,初心实例化一遍(类似于重新new一遍)。
- 除非这个方法确实想修改通过参数传入的对象,否则在类中 直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对 象创建别名。如果不确定,那就创建副本。这样客户会少些麻烦。
8.5 del和垃圾回收
对象绝不会自行销毁;然而,无法得到对象时,可能会被当作垃圾回收。
del 语句删除名称,而不是对象。del 命令可能会导致对象被当作垃圾 回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得 到对象时。2 重新绑定也可能会导致对象的引用数量归零,导致对象被 销毁。
特殊的del方法
有个 del 特殊方法,但是它不会销毁实例,不应该在 代码中调用。即将销毁实例时,Python 解释器会调用 del 方 法,给实例最后的机会,释放外部资源。自己编写的代码很少需要 实现 del 代码,有些 Python 新手会花时间实现,但却吃力不 讨好,因为 del 很难用对。详情参见 Python 语言参考手册 中“Data Model”一章中 del 特殊方法的文档 (https://docs.python.org/3/reference/datamodel.html#object.del)。
引用计数法
在 CPython 中,垃圾回收使用的主要算法是引用计数。实际上,每个对 象都会统计有多少引用指向自己。当引用计数归零时,对象立即就被销 毁:CPython 会在对象上调用 del 方法(如果定义了),然后释放 分配给对象的内存。
CPython 2.0 增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,即使再出色的引用方式也会导致组中的对象不可获取。
Python 的其他实现有更 复杂的垃圾回收程序,而且不依赖引用计数,这意味着,对象的引用数量为零时可能不会立即调用 del 方法。
为了演示对象生命结束时的情形,示例 8-16 使用 weakref.finalize 注册一个回调函数,在销毁对象时调用。
示例 8-16 没有指向对象的引用时,监视对象生命结束时的情形
import weakref
s1 = {1, 2, 3}
s2 = s1
def bye():
print('Gone with the wind...')
ender = weakref.finalize(s1, bye)
ender.alive
True
del s1
ender.alive
True
s2 = 'spam'
ender.alive
False
8.6 弱引用
正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后, 垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存 在的时间超过所需时间。这经常用在缓存中。
弱引用不会增加对象的引用数量。引用的目标对象称为所指对象 (referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象。
示例 8-17 展示了如何使用 weakref.ref 实例获取所指对象。如果对象 存在,调用弱引用可以获取对象;否则返回 None。
示例 8-17 弱引用是可调用的对象,返回的是被引用的对象;如果 所指对象不存在了,返回 None
import weakref
a_set = {0, 1}
wref = weakref.ref(a_set)
wref
<weakref at 0x000001BA5A48EC28; to 'set' at 0x000001BA5A440E48>
wref()
{0, 1}
a_set = {2, 3, 4}
a_set
{2, 3, 4}
wref()
{0, 1}
wref() is None
False
8.6.1 WeakValueDictionary简介 (缓存弱引用的缓冲池,用于构建临时变量的高速缓冲池)
WeakValueDictionary 类实现的是一种可变映射,里面的值是对象的弱引用。被引用的对象在程序中的其他地方被当作垃圾回收后,对应的 键会自动从 WeakValueDictionary 中删除。因 此,WeakValueDictionary 经常用于缓存。
WeakValueDictionary简介的相关问题:
- 时效性,临时变量引用了对象,这可能会导致该变量的存在时间比预 期长。通常,这对局部变量来说不是问题,因为它们在函数返回时 会被销毁。但是在示例 8-19 中,for 循环中的变量 cheese 是全局 变量,除非显式删除,否则不会消失。
- 与 WeakValueDictionary 对应的是 WeakKeyDictionary,后者的键 是弱引用。
- weakref 模块还提供了 WeakSet 类,按照文档的说明,这个类的作用 很简单:“保存元素弱引用的集合类。元素没有强引用时,集合会把它删除。”如果一个类需要知道所有实例,一种好的方案是创建一个 WeakSet 类型的类属性,保存实例的引用。如果使用常规的 set,实例 永远不会被垃圾回收,因为类中有实例的强引用,而类存在的时间与 Python 进程一样长,除非显式删除类。
8.6.2 弱引用的局限
不是每个 Python 对象都可以作为弱引用的目标(或称所指对象)。基本 的 list 和 dict 实例不能作为所指对象,但是它们的子类可以轻松地 解决这个问题。
其次,int 和 tuple 实例不能作为弱引用的目标,甚至它 们的子类也不行。
class MyList(list):
"""list的子类,实例可以作为弱引用的目标"""
pass
a_list = MyList(range(10))
# a_list可以作为弱引用的目标
wref_to_a_list = weakref.ref(a_list)
wref_to_a_list
<weakref at 0x000001BA57A48BD8; to 'MyList' at 0x000001BA57A48F98>
8.7 Python对不可变类型施加的把戏
对元组 t 来说,t[:] 不创建副本,而是返回同一个对 象的引用。此外,tuple(t) 获得的也是同一个元组的引用。5 示例 8- 20 证明了这一点。
示例 8-20 使用另一个元组构建元组,得到的其实是同一个元组
str、bytes 和 frozenset 实例也有这种行为
t1 = (1,2,3)
t2 = tuple(t1)
t2 is t1
True
t3 = t1[:]
t3 is t1
True
示例 8-21 字符串字面量可能会创建共享的对象(驻留问题)
t1 = (1, 2, 3)
t3 = (1, 2, 3)
t3 is t1
False
s1 = 'ABC'
s2 = 'ABC'
s2 is s1
True
共享字符串字面量是一种优化措施,称为驻留(interning)。CPython 还 会在小的整数上使用这个优化措施,防止重复创建“热门”数字,如 0、-1 和 42。
注意,CPython 不会驻留所有字符串和整数,驻留的条件 是实现细节,而且没有文档说明。
千万不要依赖字符串或整数的驻留!比较字符串或整数是否 相等时,应该使用 ==,而不是 is。驻留是 Python 解释器内部使用 的一个特性。

浙公网安备 33010602011771号