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

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

  • 8.1 变量不是盒子
  • 8.2 标识、相等性和别名
  • 8.3 默认浅拷贝
  • 8.5 del 和垃圾回收
  • 8.6 弱引用
  • 巨人的肩膀

8.1 变量不是盒子

在 Python 中变量是标签(label),不是盒子(box)。Python 中的变量是引用式变量,类似于 Java 中的引用式变量,因此把 Python 中的变量理解为附加在对象上的标签。

通过一个例子感受一下:

a = [1, 2, 3]  # a 标签指向了 [1, 2, 3] 这个列表
b = a  # 把 b 标签也指向了 [1, 2, 3] 列表
a.append(4)
print(b)
[1, 2, 3, 4]

赋值语句的右边先执行,对象在赋值之前就创建了。因此,对于引用式变量,说把变量分配给对象更合理。看下面这个例子:

class Gizmo(object):def __init__(self):print('Gizmo id: %d' % id(self))x = Gizmo()
y = Gizmo() * 10  # 可以看得到 Gizmo() 会创建一个新的 Gizmo 实例,求积失败,所以 y 变量不会创建
Gizmo id: 2236148132296
Gizmo id: 2236148132168---------------------------------------------------------------------------TypeError                                 Traceback (most recent call last)<ipython-input-6-91f1348d782b> in <module>67 x = Gizmo()
----> 8 y = Gizmo() * 10  # 可以看得到 Gizmo() 会创建一个新的 Gizmo 实例,求积失败,所以 y 变量不会创建TypeError: unsupported operand type(s) for *: 'Gizmo' and 'int'

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

8.2 标识、相等性和别名

每个对象(object)都有标识(identity)、类型(type)和值(value)。

  • 标识(identity):对象一旦创建,标识就不会改变。对象的标识具有唯一性,并且在对象的生命周期中不会改变。可以把标识理解为对象在内存中的地址。CPython 中,可以用 id() 返回对象的内存地址,用 is 比较两个对象的标识,判断它们是否是同一个对象。
  • 类型:可以用 type() 查看对象的类型。
  • 值。

别名(alias):一个对象可以有多个标签,每个标签就是一个别名。

is== 比较:

is:比较对象的标识。
==:比较对象的值。通常我们比较关注值,而不是标识,所以在 Python 代码中 == 出现的频率比 is 高。
is 运算符比 == 速度快,因为它不能重载,所以 Python 不用寻找并调用特殊方法,而是直接比较两个整数 ID。而 a == b 是语法糖,等同于 a.__eq__(b) ,相等性测试可能涉及大量处理工作。
注意:在变量和单例值之间比较时,应该使用 is。目前,最常使用 is 检查变量绑定的值是不是 None。下面是推荐的写法:

x is None# 否定的正确写法
x is not None
# 比较 is 和 ==
a = [1, 2, 3]
b = a  # a, b 都是 [1, 2, 3] 对象的别名
c = [1, 2, 3]  # 注意,这里新创建了一个 [1, 2, 3]列表对象
print(f'id(a)={id(a)}, id(b)={id(b)}, id(c)={id(c)}')
print(f'a is b: {a is b}; a == b: {a == b}')
print(f'a is c: {a is c}; a == c: {a == c}')
id(a)=2236148131656, id(b)=2236148131656, id(c)=2236156692488
a is b: True; a == b: True
a is c: False; a == c: True

重新理解元组的不可变:

元组和 Python 大多数集合类型(列表、字典、集合等)一样,都是保存的对象的引用(reference)。如果引用的对象是可变的,即便元组本身不可变,引用的对象依然可变。换句话说就是,元组的不可变性实际上是指元组数据结构的物理内容(即保存的引用)不可变,与引用的对象无关。
看下面的例子:

t1 = (1, 2, [30, 40])
t2 = (1, 2, [30, 40])
print(f'id(t1)={id(t1)}; id(t2)={id(t2)}, t1 is t2: {t1 is t2}; t1 == t2: {t1 == t2}')
print(id(t1[-1]))
t1[-1].append(99)
print(t1)
print(id(t1[-1]))
print(t1 == t2)  # t1 和 t2 值不同了
id(t1)=2236146528568; id(t2)=2236148887864, t1 is t2: False; t1 == t2: True
2236150622728
(1, 2, [30, 40, 99])
2236150622728
False

上述例子说明,元组的值会随着引用的可变对象的变化而变。元组中不可变的是元素的标识。所以有些元组是不可散列的。

8.3 默认浅拷贝

Python 中使用构造方法和切片默认做浅拷贝(shallow copy),即复制了最外层容器,副本中的元素是源容器中元素的引用。如果所有元素都是不可变的,这么做没有问题,还能节省内存。但是如果有可变元素,可能就会导致意想不到的问题。

深拷贝(deep copy):即副本不共享内部对象的引用。

copy 模块中的 deepcopy 和 copy 函数能够为任意对象做深拷贝或者浅拷贝。
关于深浅拷贝,看个例子:

# 校车乘客在途中上车和下车
import copyclass Bus(object):def __init__(self, passengers=None):if passengers is None:self.passengers = []else:self.passengers = list(passengers)def pick(self, name):self.passengers.append(name)def drop(self, name):self.passengers.remove(name)bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy.copy(bus1)  # 浅拷贝
bus3 = copy.deepcopy(bus1)  # 深拷贝
print(f' id(bus1)={id(bus1)} \n id(bus2)={id(bus2)} \n id(bus3)={id(bus3)}')  # 深浅拷贝都和原对象的 ID 不同
bus1.drop('Bill')  # bus2 也会受影响
print(f' bus1.passengers={bus1.passengers} \n bus2.passengers={bus2.passengers} \n bus3.passengers={bus3.passengers}')
# bus1 和 bus2 共用同一个passengers 列表对象
print(f''' id(bus1.passengers)={id(bus1.passengers)} \n id(bus2.passengers)={id(bus2.passengers)} \n id(bus3.passengers)={id(bus3.passengers)}''')
 id(bus1)=2236156482888id(bus2)=2236144713544id(bus3)=2236156999176bus1.passengers=['Alice', 'Claire', 'David']bus2.passengers=['Alice', 'Claire', 'David']bus3.passengers=['Alice', 'Bill', 'Claire', 'David']id(bus1.passengers)=2236144711688id(bus2.passengers)=2236144711688id(bus3.passengers)=2236156482632

注意:一般来说,深拷贝不是件简单的事。如果有对象有循环引用(cyclic reference),那么这个朴素的算法会进入无限循环。 deepcopy 函数会记住已经复制的对象,因此能够优雅的处理循环引用。
如下面这个例子:

# 循环引用:b 引用 a,然后追加到 a 中;deepcopy 会想办法复制 a
from copy import deepcopya = [10, 20]
b = [a, 30]
a.append(b)
print(a)
c = deepcopy(a)
print(c)
[10, 20, [[...], 30]]
[10, 20, [[...], 30]]

深拷贝有时可能太深,对象可能会引用不该赋值的外部资源或单例值。我们可以实现 __copy__()__deepcopy__() 特殊方法,控制 copy 和 deepcopy 的行为。具体参考 copy 模块文档。

Python 唯一支持的参数传递模式是共享传参(call by sharing)。Java 中引用类型是传引用,基本类型是按值传参。
共享传参值函数的各个形参获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。

这个方案的结果是:函数可能会修改作为参数传入的可变对象,但是无法修改这些对象的标识(即不能把一个对象替换成另一个对象)。下面这个例子展示了把数字、列表、元组传入函数,实际传入的实参会以不同的方式受到影响:

# 函数可能会修改接收到的人和可变对象
def f(a, b):a += breturn ax, y = 1, 2
print(f(x, y))
print(x, y)  # 数字 x 不变
a, b = [1, 2], [3, 4]
print(f(a, b))
print(a, b)  # 列表 a 变了
t, u = (10, 20), (30, 40)
print(f(t, u))
print(t, u)  # 元组 t 不变
3
1 2
[1, 2, 3, 4]
[1, 2, 3, 4] [3, 4]
(10, 20, 30, 40)
(10, 20) (30, 40)

不要使用可变类型作为参数的默认值。
以下例子说明可变默认值的危险:

# 一个简单的类,说明可变默认值的危险class HauntedBus(object):"""备受幽灵乘客折磨的校车"""def __init__(self, passengers=[]):  # 没有传入passengers参数,使用默认绑定的列表对象,一开始是空列表self.passengers = passengersdef pick(self, name):self.passengers.append(name)def drop(self, name):self.passengers.remove(name)bus1 = HauntedBus(['Alice', 'Bill'])
print(bus1.passengers)
bus1.pick('Charlie')
bus1.drop('Alice')
print(bus1.passengers)  # 到这里都一切正常
bus2 = HauntedBus()  # 一开始bus2是空的,因此会把默认的空列表赋值给self.passengers
bus2.pick('Carrie')
print(bus2.passengers)
bus3 = HauntedBus()  # bus3一开始也是空的,因此还是赋值默认的列表
print(bus3.passengers)  # 但是默认列表不为空!
bus3.pick('Dave')
print(bus2.passengers)  # 登上bus3 的Dave也出现在了bus2
print(bus2.passengers is bus3.passengers)  # bus2.passengers 和 bus3.passengers 指向同一个列表
print(bus1.passengers)  # 但是bus1.passengers是不同的列表
['Alice', 'Bill']
['Bill', 'Charlie']
['Carrie']
['Carrie']
['Carrie', 'Dave']
True
['Bill', 'Charlie']

这里的问题在于,没有指定初始乘客的 HauntedBus 实例会共享同一个乘客列表。这种问题很难发现。出现这个问题的根源是,默认值在定义函数时计算(通常在加载模块时),因此默认值变成了函数对象的属性。因此,如果默认值是可变对象,而且修改了它的值,那么后续的函数调用都会受到影响。

我们可以审查 HauntedBus.__init__ 对象,看看它的 __defaults__ 属性有哪些幽灵学生。

print(dir(HauntedBus.__init__))
print(HauntedBus.__init__.__defaults__)
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
(['Carrie', 'Dave'],)

最后,可以验证 bus2.passengers 是一个别名,它绑定到 HauntedBus.HauntedBus.__init__.__defaults__ 属性的第一个元素上。

print(HauntedBus.__init__.__defaults__[0] is bus2.passengers)
True

因此通常使用 None 作为接收可变值参数的默认值。因此我们需要在 __init__ 方法做检查,如果 passengers 是 None,就把一个新的空列表赋值给 self.passengers,如果不是,正确的实现是把passengers的副本赋值给 self.passengers

下面这个 TwilightBus 实例与客户共享乘客列表,这会产生意外的结果,如下:

# 一个简单的类,说明接收可变参数的风险
class TwilightBus(object):"""让乘客销声匿迹的校车"""def __init__(self, passengers=None):if passengers is None:self.passengers = []  # 这里谨慎处理,当passengers为None时,创建一个新的空列表else:self.passengers = passengers  # self.passengers 变成了passengers的别名,即实参的别名def pick(self, name):self.passengers.append(name)  # 会修改传入的 passengers 列表def drop(self, name):self.passengers.remove(name)  # 会修改传入的 passengers 列表basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = TwilightBus(basketball_team)
bus.drop('Tina')
bus.drop('Pat')
print(basketball_team)  # basketball_team 被改变了
['Sue', 'Maya', 'Diana']

这里的 TwilightBus 违反了设计接口的最佳实践,即“最少惊讶原则”。学生下车后,她的名字就从篮球队的名单消失了,这不是我们想要的。
这里的问题是,校车为传给构造方法的列表创建了别名。
正确做法是,校车自己维护乘客列表。因此我们只需要把参数值的副本赋值给 self.passengers 就可以了。

def __init__(self, passengers=None):if passengers is None:self.passengers = []else:self.passengers = list(passengers)  # 创建passengers列表的副本,如果不是列表,就把它转换成列表

这种处理方式更加灵活,passengers 可以是任何可迭代对象。

建议:除非这个方法确实想修改通过参数传入的对象,否则在类中直接把参数赋值给实例变量之前一定要三思,因为这样会为参数对象创建别名。如果不确定,那就创建副本。减少客户的麻烦。

8.5 del 和垃圾回收

del 语句时删除名称,而不是对象。del 命令可能会导致对象被当做垃圾回收,但是仅当删除的变量保存的是对象最后一个引用,或者无法得到对象时。

重新绑定也可能会导致对象的引用数量归零,导致对象被销毁。

在 CPython 中垃圾回收使用的主要算法是引用计数。每个对象都会统计有多少个引用指向自己。当引用计数归 0 时,对象立即被销毁:CPython 会在对象上调用 __del__方法(如果定义了),然后释放分配给对象的内存。CPython 2.0 增加了分代垃圾回收算法,用于检测引用循环中涉及的对象组——如果一组对象之间全是相互引用,即使再出色的引用方式也会导致组中的对象不可获取。

8.6 弱引用

弱引用(weak references):弱引用不会增加对象的引用数量。弱引用的目标对象称为所指对象(referent)。所以弱引用不会妨碍所指对象被当做垃圾回收。

弱引用在缓存应用中非常有用。因为我们不想仅因为被缓存引用着而始终保存缓存对象。

弱引用是可调用的对象,返回的是被引用的对象;如果所指对象不存在了,返回 None。

弱引用的局限:不是每个 Python 对象都能作为弱引用的目标。基本的 list 和 dict 实例不能作为所指对象,但它们的子类可以轻松解决这个问题。
set 实例和用户定义的类型可以作为弱引用的目标。但是 int 和 tuple 实例不能作为弱引用的目标,甚至它们的子类也不行。

巨人的肩膀

  1. 《Fluent Python》
  2. 《流畅的 Python》

后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技,扫描下方二维码或者搜索每日学一技关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!

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

  1. 【Python学习笔记】第一章基础知识:格式化输出,转义字符,变量类型转换,算术运算符,运算符优先级和赋值运算符,逻辑运算符,世界杯案例题目,条件判断if语句,猜拳游戏与三目运算符

    Python学习笔记之[第一章]基础知识 前言: 一.格式化输出 1.基本格式: 2.练习代码: 二.转义字符 1.基本格式: 2.练习代码: 3.输出结果: 三.输入 1.基本格式: 2.练习代码: ...

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

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

  3. Python学习笔记__13.2章 requests

    # 这是学习廖雪峰老师python教程的学习笔记 相比于Python内置的urllib模块,使用requests可以更好地处理URL资源. 1.使用requests 1)通过GET访问一个页面 > ...

  4. Python学习笔记__6.1章 类和实例

    # 这是学习廖雪峰老师python教程的学习笔记 1.概览 面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一 ...

  5. Python学习笔记__1.5章 循环

    # 这是学习廖雪峰老师python教程的学习笔记 1.for循环遍历 1.遍历名字 names = ['Michael', 'Bob', 'Tracy'] for name in names: pri ...

  6. Python学习笔记__4.1章 高阶函数

    # 这是学习廖雪峰老师python教程的学习笔记 1.概览 我们知道Python内置的求绝对值的函数是abs() # 调用abs()函数可以获得一个值 >>> abs(-10) 10 ...

  7. Python学习笔记__10.4章 进程VS线程

    # 这是学习廖雪峰老师python教程的学习笔记 1.概览 我们介绍了多进程和多线程,这是实现多任务最常用的两种方式.现在,我们来讨论一下这两种方式的优缺点 要实现多任务,通常我们会设计Master- ...

  8. Head First Python 学习笔记(第二章:分享你的代码)

    共享你的代码 Python提供了一组技术,可以很容易地实现共享,这包括模块和一些发布工具: 模块允许你合力组织代码来实现最优共享. 发布工具允许你向全世界共享你的模块. 函数转换为模块 1.把第一章中 ...

  9. 深度之眼 - Python学习笔记——第四章 组合数据类型

    第四章 组合数据类型 4.1 列表 列表是可变的! 4.1.1 列表的表达 序列类型:内部元素有位置关系,能通过位置序号访问其中元素 列表是一个可以使用多种类型元素,支持元素的增.删.查.改操作的序列 ...

  10. Python学习笔记 ---第三章

    函数 函数是代码的一种抽象 函数 说明 abs 绝对值 max 最大值 hex 转换为16进制 强制数据类型转换 int('123') 123 int(12.35) 12 srt(100) '100' ...

最新文章

  1. 在网络中同时使用kfold和使用Dropout(基于Iris数据集)
  2. 更改用户密码oracle,oracle数据库更改用户密码
  3. vista任务栏透明_增加Windows Vista任务栏预览大小的赏金(付费!)
  4. JEECG 技术交流群
  5. mysql创建表语句和修改表语句
  6. Windows下使用pthread
  7. 软件开发中的成本意识
  8. android activity使用,Android Activity使用拾遗
  9. 使用Fresco实现简单的显示一张图片
  10. HenCoder Android 自定义 View 1-6:属性动画(上手篇)
  11. Behavior Designer 行为树中文版教程
  12. excel如何把顺序倒过来_excel表格数据前后顺序颠倒-求助:怎样使excel的数据顺序倒过来...
  13. 经营计划与经营利润分析动态报表的实现--业务需求
  14. ps-黑白老照片快速上色
  15. 在GridControl表格控件中实现多层级主从表数据的展示
  16. wxj项目的开发一点记录
  17. BoardCast BroadcastReceiver 基础
  18. 最新全国机场名(持续更新2017-12-27)
  19. 业务系统成功微服务化改造的实施步骤
  20. 亨利气体溶解度优化算法(Matlab代码实现)

热门文章

  1. 电子综合实践_触摸小台灯
  2. 钉钉移动端和PC免登
  3. LeetCode刷题笔记 - 175.Combine Two Tables
  4. 广义相对论基础【3】平移联络
  5. 【文献阅读】翻译王军武老师的文献--《稠密气固流的连续理论现状回顾》三、第二节 气固流态化的多尺度模拟
  6. DisplayTag应用总结
  7. fest556_FEST-Swing 1.2发布
  8. 【软件测试】基础知识笔记(个人用)
  9. 给定字符串A和B,输出A和B中的最大公共子串。
  10. 标签设计(CodeSoft简单使用)