《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 解释器内部使用 的一个特性。

posted @ 2021-02-14 22:12  山枫叶纷飞  阅读(121)  评论(0)    收藏  举报