《Fluent Python》学习笔记:第 8 章 对象引用、可变性和垃圾回收
本文主要是 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 实例不能作为弱引用的目标,甚至它们的子类也不行。
巨人的肩膀
- 《Fluent Python》
- 《流畅的 Python》
后记:
我从本硕药学零基础转行计算机,自学路上,走过很多弯路,也庆幸自己喜欢记笔记,把知识点进行总结,帮助自己成功实现转行。
2020下半年进入职场,深感自己的不足,所以2021年给自己定了个计划,每日学一技,日积月累,厚积薄发。
如果你想和我一起交流学习,欢迎大家关注我的微信公众号每日学一技
,扫描下方二维码或者搜索每日学一技
关注。
这个公众号主要是分享和记录自己每日的技术学习,不定期整理子类分享,主要涉及 C – > Python – > Java,计算机基础知识,机器学习,职场技能等,简单说就是一句话,成长的见证!
《Fluent Python》学习笔记:第 8 章 对象引用、可变性和垃圾回收相关推荐
- 【Python学习笔记】第一章基础知识:格式化输出,转义字符,变量类型转换,算术运算符,运算符优先级和赋值运算符,逻辑运算符,世界杯案例题目,条件判断if语句,猜拳游戏与三目运算符
Python学习笔记之[第一章]基础知识 前言: 一.格式化输出 1.基本格式: 2.练习代码: 二.转义字符 1.基本格式: 2.练习代码: 3.输出结果: 三.输入 1.基本格式: 2.练习代码: ...
- 流畅的python 对象引用 可变性和垃圾回收
对象引用.可变性和垃圾回收 变量不是盒子 人们经常使用"变量是盒子"这样的比喻,但是这有碍于理解面向对象语言中的引用式变量.Python 变量类似于 Java 中的引用式变量,因此 ...
- Python学习笔记__13.2章 requests
# 这是学习廖雪峰老师python教程的学习笔记 相比于Python内置的urllib模块,使用requests可以更好地处理URL资源. 1.使用requests 1)通过GET访问一个页面 > ...
- Python学习笔记__6.1章 类和实例
# 这是学习廖雪峰老师python教程的学习笔记 1.概览 面向对象最重要的概念就是类(Class)和实例(Instance),必须牢记类是抽象的模板,比如Student类,而实例是根据类创建出来的一 ...
- Python学习笔记__1.5章 循环
# 这是学习廖雪峰老师python教程的学习笔记 1.for循环遍历 1.遍历名字 names = ['Michael', 'Bob', 'Tracy'] for name in names: pri ...
- Python学习笔记__4.1章 高阶函数
# 这是学习廖雪峰老师python教程的学习笔记 1.概览 我们知道Python内置的求绝对值的函数是abs() # 调用abs()函数可以获得一个值 >>> abs(-10) 10 ...
- Python学习笔记__10.4章 进程VS线程
# 这是学习廖雪峰老师python教程的学习笔记 1.概览 我们介绍了多进程和多线程,这是实现多任务最常用的两种方式.现在,我们来讨论一下这两种方式的优缺点 要实现多任务,通常我们会设计Master- ...
- Head First Python 学习笔记(第二章:分享你的代码)
共享你的代码 Python提供了一组技术,可以很容易地实现共享,这包括模块和一些发布工具: 模块允许你合力组织代码来实现最优共享. 发布工具允许你向全世界共享你的模块. 函数转换为模块 1.把第一章中 ...
- 深度之眼 - Python学习笔记——第四章 组合数据类型
第四章 组合数据类型 4.1 列表 列表是可变的! 4.1.1 列表的表达 序列类型:内部元素有位置关系,能通过位置序号访问其中元素 列表是一个可以使用多种类型元素,支持元素的增.删.查.改操作的序列 ...
- Python学习笔记 ---第三章
函数 函数是代码的一种抽象 函数 说明 abs 绝对值 max 最大值 hex 转换为16进制 强制数据类型转换 int('123') 123 int(12.35) 12 srt(100) '100' ...
最新文章
- 在网络中同时使用kfold和使用Dropout(基于Iris数据集)
- 更改用户密码oracle,oracle数据库更改用户密码
- vista任务栏透明_增加Windows Vista任务栏预览大小的赏金(付费!)
- JEECG 技术交流群
- mysql创建表语句和修改表语句
- Windows下使用pthread
- 软件开发中的成本意识
- android activity使用,Android Activity使用拾遗
- 使用Fresco实现简单的显示一张图片
- HenCoder Android 自定义 View 1-6:属性动画(上手篇)
- Behavior Designer 行为树中文版教程
- excel如何把顺序倒过来_excel表格数据前后顺序颠倒-求助:怎样使excel的数据顺序倒过来...
- 经营计划与经营利润分析动态报表的实现--业务需求
- ps-黑白老照片快速上色
- 在GridControl表格控件中实现多层级主从表数据的展示
- wxj项目的开发一点记录
- BoardCast BroadcastReceiver 基础
- 最新全国机场名(持续更新2017-12-27)
- 业务系统成功微服务化改造的实施步骤
- 亨利气体溶解度优化算法(Matlab代码实现)