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

“你不开心,”白骑士用一种忧虑的声调说,“让我给你唱一首歌安
慰你吧……这首歌的曲名叫作 :《黑线鳕的眼睛》。”
“哦,那是一首歌的曲名,是吗?”爱丽丝问道,她试着使自己感到
有兴趣。
“不,你不明白,”白骑士说,看来有些心烦的样子,“那是人家这
么叫的曲名。真正的曲名是《老而又老的老头儿》。”(改编自第
8 章“这是我自己的发明”)
——Lewis Carroll《爱丽丝镜中奇遇记》


8.1 变量不是盒子

  • 引子

    • 人们经常使用”变量是盒子”这样的比喻,但是这有碍于理解面向对象语言中的引用式变量
    • Python变量类似于Java中的引用式变量,因此最好把它们理解为附加在对象上的标注
  • 示例 8-1 变量ab引用同一个列表,而不是那个列表的副本

    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.passengersbus2.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][对象引用、可变性和垃圾回收]相关推荐

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

    对象引用.可变性和垃圾回收 变量不是盒子 人们经常使用"变量是盒子"这样的比喻,但是这有碍于理解面向对象语言中的引用式变量.Python 变量类似于 Java 中的引用式变量,因此 ...

  2. 记录学习《流畅的python》的一些知识-----对象引用,可变性和垃圾回收

    记录我学习<流畅的python>的过程--对象引用,可变性和垃圾回收 2021.9.22 1.变量不是盒子 2.标识.相等性和别名 3.默认做浅复制 4.函数的参数作为引用时 5.del和 ...

  3. 流畅的Python读书笔记-第八章-对象引用、可变性和垃圾回收

    第8章:对象引用,可变性和垃圾回收 在Python里面变量不是盒子,而是便利贴,类似于Java中的引用变量,因此最好把它们理解为附加在对象上的标注. 因为变量不过是标注,因此无法阻止为对象贴上多个标注 ...

  4. 《Fluent Python》学习笔记:第 8 章 对象引用、可变性和垃圾回收

    本文主要是 Fluent Python 第 8 章的学习笔记.这部分主要是介绍了变量.引用.对象.深拷贝.浅拷贝.垃圾回收等.本章虽然枯燥,但是非常有用. <Fluent Python>学 ...

  5. python基础编程:基于Python对象引用、可变性和垃圾回收详解

    下面小编就为大家带来一篇基于Python对象引用.可变性和垃圾回收详解.小编觉得挺不错的,现在就分享给大家,也给大家做个参考.一起跟随小编过来看看吧 变量不是盒子 在示例所示的交互式控制台中,无法使用 ...

  6. 第八章《对象引用、可变性和垃圾回收》(下)

    对<流畅的python>的读书笔记以及个人理解 9-20 接着上一篇的内容,上一篇的链接:https://blog.csdn.net/scrence/article/details/100 ...

  7. 第8章 对象引用、可变性和垃圾回收

    1 #<流畅的Python>读书笔记 2 # 第四部分 面向对象惯用法 3 # 第8章 对象引用.可变性和垃圾回收 4 5 # 8.1 变量不是盒子 6 # Python 变量类似于 Ja ...

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

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

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

    对象引用.可变性和垃圾回收 变量不是盒子 变量不是盒子,而是便利贴,python是先创建对象然后再将变量赋值给对象,当创建对象之后,可以通过id查看对象的内存地址. 例如: class Gizmo:d ...

最新文章

  1. 再见 FTP/SFTP,是时候拥抱下一代文件传输利器 Croc 了!
  2. java学习笔记(12) —— Struts2 通过 xml /json 实现简单的业务处理
  3. 理解GBDT算法(三)——基于梯度的版本
  4. 【常用】截取相机图片截图功能
  5. JAVA 边界布局管理器
  6. 解决:Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x00000000c5330000, 986513408,
  7. u-boot移植随笔:解决引导内核遇到undefined instruction的错误
  8. [警告] multi-字符 character constant [-Wmultichar] ----字符+符号输出错误
  9. pe怎么删除linux文件夹,PE环境下,面对无法删除的文件夹怎么办?
  10. 随笔小杂记(一)——更改整个文件夹内的命名
  11. 海思Hi3559A Sample_comm_vdec模块解码 视频解码解析
  12. 宏基因组公众号4年精华文章目录,收藏贴(2021.1更新)
  13. Mixed supervision for surface-defect detection: from weakly to fully supervised learning:表面缺陷检测的混合监督
  14. 谷歌Android笔记本,运行安卓+Chrome OS合体新系统:谷歌Pixel 3笔记本被曝光
  15. 关于python语言和人工智能以下哪个说法不正确_民用建筑包括()和()。
  16. 廖老师的Python教程——Python简介
  17. 网络中丢包的原因及类型
  18. 关于vs新建项目时只有空白项
  19. 对于搞钱我们是认真的
  20. [前端优化]基于H5移动端优化总结

热门文章

  1. 大数据可视化陈为智慧树_智慧树知到_大数据可视化_答案章节单元测试答案
  2. Unity中的资源管理-几种常见的序列化方式
  3. 【解决方案】EasyNVR海量安防设备接入实时直播+云端(服务器)录像的实现
  4. [BUUCTF]PWN——[HarekazeCTF2019]baby_rop2
  5. 深度揭密:2015年微商怎么玩才赚钱?
  6. 双向链表插入、删除操作
  7. 匹兹堡大学申请条件计算机科学,匹兹堡大学计算机科学排名第62(2020年TFE美国排名)...
  8. 软件项目管理和测试题,软件项目管理第一章测试题
  9. SQL Server 2016 [修改数据库名称]及[添加次文件和日志文件]
  10. CAD随机孔隙3D插件 孔隙结构 多孔结构模型