[流畅的Python][8][对象引用、可变性和垃圾回收]
第8章 对象引用、可变性和垃圾回收
“你不开心,”白骑士用一种忧虑的声调说,“让我给你唱一首歌安
慰你吧……这首歌的曲名叫作 :《黑线鳕的眼睛》。”
“哦,那是一首歌的曲名,是吗?”爱丽丝问道,她试着使自己感到
有兴趣。
“不,你不明白,”白骑士说,看来有些心烦的样子,“那是人家这
么叫的曲名。真正的曲名是《老而又老的老头儿》。”(改编自第
8 章“这是我自己的发明”)
——Lewis Carroll《爱丽丝镜中奇遇记》
8.1 变量不是盒子
引子
- 人们经常使用”变量是盒子”这样的比喻,但是这有碍于理解面向对象语言中的引用式变量
- Python变量类似于Java中的引用式变量,因此最好把它们理解为附加在对象上的标注
示例 8-1 变量
a
和b
引用同一个列表,而不是那个列表的副本1 2 3 4 5
>>> a = [1, 2, 3] >>> b = a >>> a.append(4) >>> b [1, 2, 3, 4]
如图
变量的赋值方式
- 对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了
- 为了理解Python中的赋值语句,应该始终先读右边。对象在右边创建或获取,在此之后左边的变量才会绑定到对象上,就像为对象贴上便利贴一样
8.2 标识、相等性和别名
别名
- 因为变量不过是标注,所以无法阻止为对象贴上多个标注
- 贴的多个标注,就是别名
示例
- 背景:Lewis Carroll 是 Charles Lutwidge Dodgson 教授的笔名。Carroll 先生指的就是 Dodgson 教授,二者是同一个人。然而,假如有冒充者(姑且叫他 Alexander Pedachenko 博士)生于 1832年,声称他是 Charles L. Dodgson。这个冒充者的证件可能一样,但 Pedachenko 博士不是Dodgson 教授
- 如图
- 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
>>> charles = {'name': 'Charles L. Dodgson', 'born': 1832} # lewis 是 charles 的别名 >>> lewis = charles >>> lewis is charles True # is 运算符和 id 函数确认了这一点 >>> id(charles), id(lewis) (4300473992, 4300473992) # 向 lewis 中添加一个元素相当于向 charles 中添加一个元素 >>> lewis['balance'] = 950 >>> charles {'name': 'Charles L. Dodgson', 'balance': 950, 'born': 1832} # alex 指代的对象与赋值给 charles 的对象内容一样 >>> alex = {'name': 'Charles L. Dodgson', 'born': 1832, 'balance': 950} # 比较两个对象,结果相等,这是因为 dict 类的 __eq__ 方法就是这样实现的 >>> alex == charles True # 但它们是不同的对象。这是 Python 说明标识不同的方式:a is not b >>> alex is not charles True
标识
- 每个对象都有标识、类型和值
- 对象一旦创建,它的标识绝不会变;你可以把标识理解为对象在内存中的地址
is
运算符比较两个对象的标识id()
函数返回对象标识的整数表示,在CPython中id()
返回对象的内存地址
8.2.1 在 == 和 is 之间选择
简介
==
: 比较的是两个对象的值(对象中保存的数据)is
- 比较的是对象的标识
- 在变量和单例值之间比较时,应该用
is
1 2 3 4
>>> x is None True >>> x is not None False
比较
is
运算符比==
速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数 ID- 而
a == b
是语法糖,等同于a.__eq__(b)
- 继承自
object
的__eq__
方法比较两个对象的 ID,结果与is
一样 - 但是多数内置类型使用更有意义的方式覆盖了
__eq__
方法,会考虑对象属性的值
8.2.2 元组的相对不可变性
简介
- 元组与多数 Python 集合(列表、字典、集,等等)一样,保存的是对象的引用
- 如果引用的元素是可变的,即便元组本身不可变,元素依然可变
- 也就是说,元组的不可变性其实是指
tuple
数据结构的物理内容(即保存的引用)不可变,与引用的对象无关
示例 元组中不可变的是元素的标识,元组的值会随着引用的可变对象的变化而变
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
# t1 不可变,但是 t1[-1] 可变 >>> t1 = (1, 2, [30, 40]) # 构建元组 t2,它的元素与 t1 一样 >>> t2 = (1, 2, [30, 40]) # 虽然 t1 和 t2 是不同的对象,但是二者相等——与预期相符 >>> t1 == t2 True # 查看 t1[-1] 列表的标识 >>> id(t1[-1]) 4302515784 # 就地修改 t1[-1] 列表 >>> t1[-1].append(99) >>> t1 (1, 2, [30, 40, 99]) # t1[-1] 的标识没变,只是值变了 >>> id(t1[-1]) 4302515784 # 现在,t1 和 t2 不相等 >>> t1 == t2 False
元组的相对不可变性解释了 2.6.1 节的谜题。这也是有些元组不可散列(参见 3.1 节中的“什么是可散列的数据类型”附注栏)的原因
8.3 默认做浅复制
8.3.1 浅复制
浅复制
- 只复制最外层容器,副本中的元素是源容器中元素的引用
- 如果所有元素都是不可变的,那么这样没有什么问题,还能节省内存
- 但是如果有可变元素,可能导致意想不到的问题
示例: 如何进行浅复制
1 2 3 4 5 6 7 8 9 10 11 12 13
>>> l1 = [3, [55, 44], (7, 8, 9)] # 使用构造方法进行浅复制 >>> l2 = list(l1) # 使用[:]进行浅复制 >>> l3 = l1[:] >>> l2 [3, [55, 44], (7, 8, 9)] # 副本与源列表相等 >>> l2 == l1 True # 但是二者指代不同的对象 >>> l2 is l1 False
示例 : 浅复制导致的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
l1 = [3, [66, 55, 44], (7, 8, 9)] l2 = list(l1) # l2 是 l1 的浅复制副本。此时的状态如下图所示 l1.append(100) # 把 100 追加到 l1 中,对 l2 没有影响 l1[1].remove(55) # 把内部列表 l1[1] 中的 55 删除。这对 l2 有影响,因为 l2[1] 绑定的列表与 l1[1] 是同一个 print('l1:', l1) print('l2:', l2) # 对可变的对象来说,如 l2[1] 引用的列表,+= 运算符就地修改列表。这次修改在 l1[1] 中也有体现,因为它是 l2[1] 的别名 l2[1] += [33, 22] # 对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量l2[2] l2[2] += (10, 11) print('l1:', l1) print('l2:', l2)
所以,浅复制虽然容易操作,但是有时得到的结果并不是你想要的
8.3.2 为任意对象做深复制和浅复制
深复制:副本不共享内部对象的引用
copy模块
- copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制
一般来说,深复制不是件简单的事。如果对象有循环引用,那么这个朴素的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用
深复制有时可能太深了。例如,对象可能会引用不该复制的外部资源或单例值。我们可以实现特殊方法
__copy__()
和__deepcopy__()
,控制 copy 和 deepcopy 的行为
8.4 函数的参数作为引用时
Python传参的方案
- Python唯一支持的传参方式是共享传参(call by sharing)
- 共享传参是指:函数的各个形式参数获得实参中各个引用的副本,也就是说,函数内部的形参是实参的别名
- 这种方案的结果是,函数可能修改作为参数传入的可变对象,但是无法修改那些对象的标识。也就是说传入的参数可能会受到函数的影响而改变
示例:函数可能会修改接收到的可变对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
>>> def f(a, b): ... a += b ... return a ... >>> x = 1 >>> y = 2 >>> f(x, y) 3 >>> # 数字x没变 >>> x, y 1, 2 >>> a = [1, 2] >>> b = [3, 4] >>> f(a, b) [1, 2, 3, 4] >>> # 列表a变了 >>> a, b ([1, 2, 3, 4], [3, 4]) >>> t = (10, 20) >>> u = (30, 40) >>> f(t, u) (10, 20, 30, 40) >>> # 元组t没变 >>> t, u ((10, 20), (30, 40))
8.4.1 不要使用可变类型作为参数的默认值
背景:可选参数可以有默认值,这是 Python 函数定义的一个很棒的特性,这样我们的 API 在进化的同时能保证向后兼容。然而,我们应该避免使用可变的对象作为参数的默认值
示例:幽灵列车,它将说明为什么应该避免使用可变的对象作为参数的默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
class HautedBus:"""备受幽灵乘客折磨的校车"""# 如果没传入 passengers 参数,使用默认绑定的列表对象,一开始是空列表def __init__(self, passengers=[]):# 这个赋值语句把 self.passengers 变成 passengers 的别名,而没有传入 passengers 参数时,后者又是默认列表的别名self.passengers = passengersdef pick(self, name):# 在 self.passengers 上调用 .remove() 和 .append() 方法时,修改的其实是默认列表,它是函数对象的一个属性self.passengers.append(name)def drop(self, name):self.passengers.remove(name)if __name__ == '__main__':# 一开始,bus1 是空的,因此把默认的空列表赋值给self.passengersbus1 = HautedBus()bus1.pick('peter')# bus2 一开始也是空的,因此还是赋值默认的列表# 但是默认列表不为空!bus2 = HautedBus()bus2.pick('alice')# bus1和bus2共享了乘客print(bus1.passengers)print(bus2.passengers)print(bus1.passengers is bus2.passengers)# 最后发现默认参数实际上是绑定在类的init方法的defaults属性上,作为一个类的属性,它只有一份,不会拷贝print(bus1.passengers is HautedBus.__init__.__defaults__[0])
解释:为什么应该避免使用可变的对象作为参数的默认值
- 默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性(上例中是
HautedBus
类的init
方法上的default属性
) - 因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响
- 其实
bus1.passengers
和bus2.passengers
都是别名,它绑定到HauntedBus.__init__.__defaults__
属性的第一个元素上
- 默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性(上例中是
8.4.2 防御可变参数
简介
- 如果定义的函数接收可变参数,应该谨慎考虑调用者是否期望改变传入的参数
- 例如,如果函数接收一个字典,而且在处理的过程中要修改它,那么这个副作用要不要体现到函数外部
示例:消失的篮球队
1 2 3 4 5 6 7 8 9 10
# basketball_team 中有 5 个学生的名字 >>> basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat'] # 使用这队学生实例化 TwilightBus >>> bus = TwilightBus(basketball_team) # 一个学生从 bus 下车了,接着又有一个学生下车了 >>> bus.drop('Tina') >>> bus.drop('Pat') # 下车的学生从篮球队中消失了 >>> basketball_team ['Sue', 'Maya', 'Diana']
- TwilightBus 违反了设计接口的最佳实践,即“最少惊讶原则”
- 学生从校车中下车后,她的名字就从篮球队的名单中消失了
解决:
- 除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思
- 因为这样会为参数对象创建别名
- 如果不确定,那就创建副本。这样客户会少些麻烦
8.5 del和垃圾回收
对象绝不会自行销毁;然而,无法得到对象时,可能会被当做垃圾回收
del
简介del
语句删除名称,而不是对象del
命令可能会导致对象被当作垃圾回收,但是仅当删除的变量保存的是对象的最后一个引用,或者无法得到对象时
__del__
特殊方法- 有个
__del__
特殊方法,但是它不会销毁实例,不应该在代码中调用 - 即将销毁实例时,Python 解释器会调用
__del__
方法,给实例最后的机会,释放外部资源
- 有个
垃圾回收算法
- 在 CPython 中,垃圾回收使用的主要算法是引用计数
- 实际上,每个对象都会统计有多少引用指向自己
- 当引用计数归零时,对象立即就被销毁:CPython 会在对象上调用
__del__
方法(如果定义了),然后释放分配给对象的内存
总结
del
不会删除对象,但是执行del
操作后可能导致对象不可获取从而被删除
[流畅的Python][8][对象引用、可变性和垃圾回收]相关推荐
- 流畅的python 对象引用 可变性和垃圾回收
对象引用.可变性和垃圾回收 变量不是盒子 人们经常使用"变量是盒子"这样的比喻,但是这有碍于理解面向对象语言中的引用式变量.Python 变量类似于 Java 中的引用式变量,因此 ...
- 记录学习《流畅的python》的一些知识-----对象引用,可变性和垃圾回收
记录我学习<流畅的python>的过程--对象引用,可变性和垃圾回收 2021.9.22 1.变量不是盒子 2.标识.相等性和别名 3.默认做浅复制 4.函数的参数作为引用时 5.del和 ...
- 流畅的Python读书笔记-第八章-对象引用、可变性和垃圾回收
第8章:对象引用,可变性和垃圾回收 在Python里面变量不是盒子,而是便利贴,类似于Java中的引用变量,因此最好把它们理解为附加在对象上的标注. 因为变量不过是标注,因此无法阻止为对象贴上多个标注 ...
- 《Fluent Python》学习笔记:第 8 章 对象引用、可变性和垃圾回收
本文主要是 Fluent Python 第 8 章的学习笔记.这部分主要是介绍了变量.引用.对象.深拷贝.浅拷贝.垃圾回收等.本章虽然枯燥,但是非常有用. <Fluent Python>学 ...
- python基础编程:基于Python对象引用、可变性和垃圾回收详解
下面小编就为大家带来一篇基于Python对象引用.可变性和垃圾回收详解.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧 变量不是盒子 在示例所示的交互式控制台中,无法使用 ...
- 第八章《对象引用、可变性和垃圾回收》(下)
对<流畅的python>的读书笔记以及个人理解 9-20 接着上一篇的内容,上一篇的链接:https://blog.csdn.net/scrence/article/details/100 ...
- 第8章 对象引用、可变性和垃圾回收
1 #<流畅的Python>读书笔记 2 # 第四部分 面向对象惯用法 3 # 第8章 对象引用.可变性和垃圾回收 4 5 # 8.1 变量不是盒子 6 # Python 变量类似于 Ja ...
- Fluent_Python_Part4面向对象,08-ob-ref,对象引用、可变性和垃圾回收
第四部分第8章,对象引用.可变性和垃圾回收 1. 创建对象之后才会把变量分配给对象 变量是对象的标注,是对象的别名,是对象的引用,并不是对象存储的地方. 例子1. 证明赋值语句的右边先执行 class ...
- 对象引用、可变性和垃圾回收
对象引用.可变性和垃圾回收 变量不是盒子 变量不是盒子,而是便利贴,python是先创建对象然后再将变量赋值给对象,当创建对象之后,可以通过id查看对象的内存地址. 例如: class Gizmo:d ...
最新文章
- 再见 FTP/SFTP,是时候拥抱下一代文件传输利器 Croc 了!
- java学习笔记(12) —— Struts2 通过 xml /json 实现简单的业务处理
- 理解GBDT算法(三)——基于梯度的版本
- 【常用】截取相机图片截图功能
- JAVA 边界布局管理器
- 解决:Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000c5330000, 986513408,
- u-boot移植随笔:解决引导内核遇到undefined instruction的错误
- [警告] multi-字符 character constant [-Wmultichar] ----字符+符号输出错误
- pe怎么删除linux文件夹,PE环境下,面对无法删除的文件夹怎么办?
- 随笔小杂记(一)——更改整个文件夹内的命名
- 海思Hi3559A Sample_comm_vdec模块解码 视频解码解析
- 宏基因组公众号4年精华文章目录,收藏贴(2021.1更新)
- Mixed supervision for surface-defect detection: from weakly to fully supervised learning:表面缺陷检测的混合监督
- 谷歌Android笔记本,运行安卓+Chrome OS合体新系统:谷歌Pixel 3笔记本被曝光
- 关于python语言和人工智能以下哪个说法不正确_民用建筑包括()和()。
- 廖老师的Python教程——Python简介
- 网络中丢包的原因及类型
- 关于vs新建项目时只有空白项
- 对于搞钱我们是认真的
- [前端优化]基于H5移动端优化总结
热门文章
- 大数据可视化陈为智慧树_智慧树知到_大数据可视化_答案章节单元测试答案
- Unity中的资源管理-几种常见的序列化方式
- 【解决方案】EasyNVR海量安防设备接入实时直播+云端(服务器)录像的实现
- [BUUCTF]PWN——[HarekazeCTF2019]baby_rop2
- 深度揭密:2015年微商怎么玩才赚钱?
- 双向链表插入、删除操作
- 匹兹堡大学申请条件计算机科学,匹兹堡大学计算机科学排名第62(2020年TFE美国排名)...
- 软件项目管理和测试题,软件项目管理第一章测试题
- SQL Server 2016 [修改数据库名称]及[添加次文件和日志文件]
- CAD随机孔隙3D插件 孔隙结构 多孔结构模型