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

变量不是盒子

人们经常使用“变量是盒子”这样的比喻,但是这有碍于理解面向对象语言中的引用式变量。Python 变量类似于 Java 中的引用式变量,因此最好把它们理解为附加在对象上的标注。

在示例 8-1 所示的交互式控制台中,无法使用“变量是盒子”做解释。图8-1 说明了在 Python 中为什么不能使用盒子比喻,而便利贴则指出了变量的正确工作方式。

示例 8-1 变量 a 和 b 引用同一个列表,而不是那个列表的副本

>>> a = [1, 2, 3]
>>> b = a
>>> a.append(4)
>>> b
[1, 2, 3, 4]

示例 8-2 创建对象之后才会把变量分配给对象

>>> class Gizmo:
... def __init__(self):
... print('Gizmo id: %d' % id(self))
...
>>> x = Gizmo()
Gizmo id: 4301489152 ➊
>>> y = Gizmo() * 10 ➋
Gizmo id: 4301489432 ➌
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'
>>>
>>> dir() ➍
['Gizmo', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__', 'x']

❶ 输出的 Gizmo id: ... 是创建 Gizmo 实例的副作用。
❷ 在乘法运算中使用 Gizmo 实例会抛出异常。
❸ 这里表明,在尝试求积之前其实会创建一个新的 Gizmo 实例。
❹ 但是,肯定不会创建变量 y,因为在对赋值语句的右边进行求值时抛出了异常。

为了理解 Python 中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,这就像为对象贴上标注。忘掉盒子吧!

标识、相等性和别名

示例 8-3 charles 和 lewis 指代同一个对象

>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832}
>>> lewis = charles ➊
>>> lewis is charles
True
>>> id(charles), id(lewis) ➋
(4300473992, 4300473992)
>>> lewis['balance'] = 950 ➌
>>> charles
{'name': 'Charles L. Dodgson', 'balance': 950, 'born': 1832}

❶ lewis 是 charles 的别名。
❷ is 运算符和 id 函数确认了这一点。
❸ 向 lewis 中添加一个元素相当于向 charles 中添加一个元素。

然而,假如有冒充者(姑且叫他 Alex博士)生于 1832年,声称他是 Charles L. Dodgson。这个冒充者的证件可能一样,但是Pedachenko 博士不是 Dodgson 教授。这种情况如图 8-2 所示。

图 8-2:charles 和 lewis 绑定同一个对象,alex 绑定另一个具有相同内容的对象

示例 8-4 alex 与 charles 比较的结果是相等,但 alex 不是charles

>>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} ➊
>>> alex == charles ➋
True
>>> alex is not charles ➌
True

❶ alex 指代的对象与赋值给 charles 的对象内容一样。
❷ 比较两个对象,结果相等,这是因为 dict 类的 __eq__ 方法就是这样实现的。
❸ 但它们是不同的对象。这是 Python 说明标识不同的方式:a is notb。

示例 8-3 体现了别名。在那段代码中,lewis 和 charles 是别名,即两个变量绑定同一个对象。而 alex 不是 charles 的别名,因为二者绑定的是不同的对象。alex 和 charles 绑定的对象具有相同的值(== 比
较的就是值),但是它们的标识不同。

在==和is之间选择

== 运算符比较两个对象的值(对象中保存的数据),而 is 比较对象的标识。
通常,我们关注的是值,而不是标识,因此 Python 代码中 == 出现的频率比 is 高。然而,在变量和单例值之间比较时,应该使用 is。目前,最常使用 is检查变量绑定的值是不是 None。下面是推荐的写法:

x is None

否定的正确写法是

x is not None

特殊方法,而是直接比较两个整数 ID。而 a == b 是语法糖,等同于a.__eq__(b)。继承自 object 的 __eq__ 方法比较两个对象的 ID,结果与 is 一样。但是多数内置类型使用更有意义的方式覆盖了 __eq__
方法,会考虑对象属性的值。相等性测试可能涉及大量处理工作,例如,比较大型集合或嵌套层级深的结构时。在结束对标识和相等性的讨论之前,我们来看看著名的不可变类型tuple(元组),它没有你想象的那么一成不变。

元组的相对不可变性

元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用。 如果引用的元素是可变的,即便元组本身不可变,元素依然可变。也就是说,元组的不可变性其实是指 tuple 数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。

示例 8-5 一开始,t1 和 t2 相等,但是修改 t1 中的一个可变元素后,二者不相等了

>>> t1 = (1, 2, [30, 40]) ➊
>>> t2 = (1, 2, [30, 40]) ➋
>>> t1 == t2 ➌
True
>>> id(t1[-1]) ➍
4302515784
>>> t1[-1].append(99) ➎
>>> t1
(1, 2, [30, 40, 99])
>>> id(t1[-1]) ➏
4302515784
>>> t1 == t2 ➐
False

❶ t1 不可变,但是 t1[-1] 可变。
❷ 构建元组 t2,它的元素与 t1 一样。
❸ 虽然 t1 和 t2 是不同的对象,但是二者相等——与预期相符。
❹ 查看 t1[-1] 列表的标识。
❺ 就地修改 t1[-1] 列表。
❻ t1[-1] 的标识没变,只是值变了。
❼ 现在,t1 和 t2 不相等

不要使用可变类型作为参数的默认值

可选参数可以有默认值,这是 Python 函数定义的一个很棒的特性,这样我们的 API 在进化的同时能保证向后兼容。然而,我们应该避免使用可变的对象作为参数的默认值。

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)

❶ 如果没传入 passengers 参数,使用默认绑定的列表对象,一开始是空列表。

❷ 这个赋值语句把 self.passengers 变成 passengers 的别名,而没有传入 passengers 参数时,后者又是默认列表的别名。

❸ 在 self.passengers 上调用 .remove() 和 .append() 方法时,修改的其实是默认列表,它是函数对象的一个属性。HauntedBus 的诡异行为如示例 8-13 所示。

>>> bus1 = HauntedBus(['Alice', 'Bill'])
>>> bus1.passengers
['Alice', 'Bill']
>>> bus1.pick('Charlie')
>>> bus1.drop('Alice')
>>> bus1.passengers ➊
['Bill', 'Charlie']
>>> bus2 = HauntedBus() ➋
>>> bus2.pick('Carrie')
>>> bus2.passengers
['Carrie']
>>> bus3 = HauntedBus() ➌
>>> bus3.passengers ➍
['Carrie']
>>> bus3.pick('Dave')
>>> bus2.passengers ➎
['Carrie', 'Dave']
>>> bus2.passengers is bus3.passengers ➏
True
>>> bus1.passengers ➐
['Bill', 'Charlie']

❶ 目前没什么问题,bus1 没有出现异常。
❷ 一开始,bus2 是空的,因此把默认的空列表赋值给self.passengers。
❸ bus3 一开始也是空的,因此还是赋值默认的列表。
❹ 但是默认列表不为空!
❺ 登上 bus3 的 Dave 出现在 bus2 中。
❻ 问题是,bus2.passengers 和 bus3.passengers 指代同一个列表。
❼ 但 bus1.passengers 是不同的列表。

问题在于,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列
表。
这种问题很难发现。如示例 8-13 所示,实例化 HauntedBus 时,如果传入乘客,会按预期运作。但是不为 HauntedBus 指定乘客的话,奇怪的事就发生了,这是因为 self.passengers 变成了 passengers 参数
默认值的别名。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。例如,如果函数接收一个字典,而且在处理的过程中要修改它,那么这个副作用要不要体现到函数外部?具体情况具体分析。这其实需要函数的编写者和调用方达成共识。

示例 8-14 从 TwilightBus 下车后,乘客消失了

>>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] ➊
>>> bus = TwilightBus(basketball_team) ➋
>>> bus.drop('Tina') ➌
>>> bus.drop('Pat')
>>> basketball_team ➍
['Sue', 'Maya', 'Diana']

❶ basketball_team 中有 5 个学生的名字。
❷ 使用这队学生实例化 TwilightBus。
❸ 一个学生从 bus 下车了,接着又有一个学生下车了。
❹ 下车的学生从篮球队中消失了!
TwilightBus 违反了设计接口的最佳实践,即“最少惊讶原则”。学生从校车中下车后,她的名字就从篮球队的名单中消失了,这确实让人惊讶。

示例 8-15 一个简单的类,说明接受可变参数的风险

class TwilightBus:
"""让乘客销声匿迹的校车"""
def __init__(self, passengers=None):
if passengers is None:
self.passengers = [] ➊
else:
self.passengers = passengers ➋
def pick(self, name):
self.passengers.append(name)
def drop(self, name):
self.passengers.remove(name) ➌

❶ 这里谨慎处理,当 passengers 为 None 时,创建一个新的空列表。
❷ 然而,这个赋值语句把 self.passengers 变成 passengers 的别名,而后者是传给 __init__ 方法的实参(即示例 8-14 中的basketball_team)的别名。
❸ 在 self.passengers 上调用 .remove() 和 .append() 方法其实会修改传给构造方法的那个列表。

这里的问题是,校车为传给构造方法的列表创建了别名。正确的做法是,校车自己维护乘客列表。修正的方法很简单:在 __init__ 中,传入 passengers 参数时,应该把参数值的副本赋值给
self.passengers,像示例 8-8 中那样做(8.3 节)。

def __init__(self, passengers=None):
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) ➊

➊ 创建 passengers 列表的副本;如果不是列表,就把它转换成列表。

在内部像这样处理乘客列表,就不会影响初始化校车时传入的参数了。此外,这种处理方式还更灵活:现在,传给 passengers 参数的值可以是元组或任何其他可迭代对象,例如 set 对象,甚至数据库查询结果,因为 list 构造方法接受任何可迭代对象。自己创建并管理列表可以确保支持所需的 .remove() 和 .append() 操作,这样 .pick() 和.drop() 方法才能正常运作。

del和垃圾回收

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

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

有个 __del__ 特殊方法,但是它不会销毁实例,不应该在代码中调用。即将销毁实例时,Python 解释器会调用 __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' ➏
Gone with the wind...
>>> ender.alive
False

❶ s1 和 s2 是别名,指向同一个集合,{1, 2, 3}。
❷ 这个函数一定不能是要销毁的对象的绑定方法,否则会有一个指向对象的引用。
❸ 在 s1 引用的对象上注册 bye 回调。
❹ 调用 finalize 对象之前,.alive 属性的值为 True。
❺ 如前所述,del 不删除对象,而是删除对象的引用。
❻ 重新绑定最后一个引用 s2,让 {1, 2, 3} 无法获取。对象被销毁了,调用了 bye 回调,ender.alive 的值变成了 False。

示例 8-16 的目的是明确指出 del 不会删除对象,但是执行 del 操作后可能会导致对象不可获取,从而被删除。

弱引用

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存
在的时间超过所需时间。这经常用在缓存中。弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。
弱引用在缓存应用中很有用,因为我们不想仅因为被缓存引用着而始终保存缓存对象

示例 8-17 是一个控制台会话,Python 控制台会自动把 _ 变量绑定到结果不为 None 的表达式结果上。这对我想演示的行为有影响,不过却凸显了一个实际问题:微观管理内存时,往往会得到意
外的结果,因为不明显的隐式赋值会为对象创建新引用。控制台中的 _ 变量是一例。调用跟踪对象也常导致意料之外的引用。

示例 8-17 弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回 None

>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set) ➊
>>> wref
<weakref at 0x100637598; to 'set' at 0x100636748>
>>> wref() ➋
{0, 1}
>>> a_set = {2, 3, 4} ➌
>>> wref() ➍
{0, 1}
>>> wref() is None ➎
False
>>> wref() is None ➏
True

❶ 创建弱引用对象 wref,下一行审查它。
❷ 调用 wref() 返回的是被引用的对象,{0, 1}。因为这是控制台会
话,所以 {0, 1} 会绑定给 _ 变量。
❸ a_set 不再指代 {0, 1} 集合,因此集合的引用数量减少了。但是 _
变量仍然指代它。
❹ 调用 wref() 依旧返回 {0, 1}。
❺ 计算这个表达式时,{0, 1} 存在,因此 wref() 不是 None。但是,
随后 _ 绑定到结果值 False。现在 {0, 1} 没有强引用了。
❻ 因为 {0, 1} 对象不存在了,所以 wref() 返回 None。

weakref 模块的文档(http://docs.python.org/3/library/weakref.html)指出,weakref.ref 类其实是低层接口,供高级用途使用,多数程序最好使用 weakref 集合和 finalize。也就是说,应该使用
WeakKeyDictionary、WeakValueDictionary、WeakSet 和finalize(在内部使用弱引用),不要自己动手创建并处理weakref.ref 实例。我们在示例 8-17 中那么做是希望借助实际使用
weakref.ref 来褪去它的神秘色彩。但是实际上,多数时候 Python 程序都使用 weakref 集合.

摘自流畅的python

流畅的python 对象引用 可变性和垃圾回收的更多相关文章

  1. 流畅的python第八章对象引用,可变性和垃圾回收

    变量不是盒子 在==和is之间选择 ==比较两个对象的值,而is比较对象的标识 元组的相对不可变姓 元组与多数的python集合(列表,字典,集,等等)一样,保存的是对象的引用.如果引用的元素是可变的 ...

  2. 基于Python对象引用、可变性和垃圾回收详解

    基于Python对象引用.可变性和垃圾回收详解 下面小编就为大家带来一篇基于Python对象引用.可变性和垃圾回收详解.小编觉得挺不错的,现在就分享给大家,也给大家做个参考. 变量不是盒子 在示例所示 ...

  3. Fluent_Python_Part4面向对象,08-ob-ref,对象引用、可变性和垃圾回收

    第四部分第8章,对象引用.可变性和垃圾回收 1. 创建对象之后才会把变量分配给对象 变量是对象的标注,是对象的别名,是对象的引用,并不是对象存储的地方. 例子1. 证明赋值语句的右边先执行 class ...

  4. Python 内存管理与垃圾回收

    Python 内存管理与垃圾回收 参考文献:https://pythonav.com/wiki/detail/6/88/ 引用计数器为主标记清除和分代回收为辅 + 缓存机制 1.1 大管家refcha ...

  5. PythonI/O进阶学习笔记_6.对象引用,可变性和垃圾回收

    前言: 没有前言了- -......这系列是整理的以前的笔记上传的,有些我自己都忘记我当时记笔记的关联关系了. 记住以后 笔记记了就是用来复习的!!!不看不就啥用没了吗!!! content: 1.p ...

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

    7.1 python变量到底是什么 #python和java中的变量本质不一样,python的变量实质上是一个指针 int str, 便利贴 a = 1 a = "abc" #1. ...

  7. Python内存管理:垃圾回收

    http://blog.csdn.net/pipisorry/article/details/39647931 Python GC主要使用引用计数(reference counting)来跟踪和回收垃 ...

  8. python内存机制与垃圾回收、调优手段

    目录 一.python的内存机制 二.python的垃圾回收 1. 引用计数 1.1 原理: 1.2 优缺点: 1.4 两种情况: 2. 标记清除 2.1 原理: 2.2 优缺点: 3. 分代回收 3 ...

  9. python内存管理及垃圾回收

    一.python的内存管理 python内部将所有类型分成两种,一种由单个元素组成,一种由多个元素组成.利用不同结构体进行区分 /* Nothing is actually declared to b ...

随机推荐

  1. flex and bison学习笔记01

    工作需要,学习一下Flex and bison,以前在编译原理的课上听老师说过他们的前辈,lex and yacc.Flex and bison就是lex and yacc的升级版. 参考书:flex ...

  2. lseek,fcntl,ioctl函数

    函数说明:每一个已打开的文件都有一个读写位置, 当打开文件时通常其读写位置是指向文件开头, 若是以附加的方式打开文件(如O_APPEND), 则读写位置会指向文件尾. 当read()或write()时 ...

  3. Paxos发展、算法原理

    Paxos 发展史 Leslie Lamport所提出的Paxos算法是现代分布式系统中的一项重要的基础性技术,得到广泛的应用. Paxos的整个发展过程大概可以分为三个阶段: 第一阶段:萌芽期,大致 ...

  4. 【Mac + Python + Selenium】之PyCharm配置Selenium自动化

    一.安装PyCharm 1.下载地址: Pycharm编辑器官网下载地址 2.安装完成之后打开,一路下去点击,直到填写注册码,参考: <[转载][Pycharm编辑器破解步骤]之idea和Pyc ...

  5. java算法-数学之美一

    巧用数学的思想来解决程序算法问题,这样的代码如诗般优美.通过数学思想来看问题,也能将程序简单化.“斐波那契数列”对于java程序员来说一定不陌生.当然这个问题的解决方案也有很多.用一个例子说明数学思想 ...

  6. noip 模拟赛 After 17(递推+特殊的技巧)

    来源:Violet_II T1 好神的一题,我竟然没做出来QAQ 首先我们发现,答案是sigma(x[i]*x[j], i>j)+sigma(y[i]*y[j], i>j).显然只需要讨论 ...

  7. 用ChemDraw画3D图的方法

    在绘制化学图形的时候,很多的用户都会发现很多的图形都是三维的,这个时候就需要找一款能够绘制3D图形的化学绘图软件.ChemOffice 15.1是最新的化学绘图工具套件,总共有三个组件,其中ChemD ...

  8. VSCode调试配置

    http://code.visualstudio.com/docs/editor/debugging#_launch-configurations VSCode内置Node.js运行时, 能调试jav ...

  9. C# .net 多线程中集合数据同步

    from:http://www.cnblogs.com/GavinCome/archive/2008/04/09/1145250.html C# .net 多线程中集合数据同步(转) 集合类通常不是线 ...

  10. 再谈iOS 7的手势滑动返回功能

    本文转载至 http://blog.csdn.net/jasonblog/article/details/28282147  之前随手写过一篇<使用UIScreenEdgePanGestureR ...