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


Posted in Python onAugust 21, 2017

变量不是盒子

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

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

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

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

如果把变量想象为盒子,那么无法解释 Python 中的赋值;应该把变量视作便利贴,这样示例中的行为就好解释了

注意:

对引用式变量来说,说把变量分配给对象更合理,反过来说就有问题。毕竟,对象在赋值之前就创建了

标识、相等性和别名

Lewis Carroll 是 Charles Lutwidge Dodgson 教授的笔名。Carroll 先生指的就是 Dodgson 教授,二者是同一个人。? 用 Python 表达了这个概念。

charles 和 lewis 指代同一个对象

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

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

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

charles 和 lewis 绑定同一个对象,alex 绑定另一个具有相同内容的对象

alex 与 charles 比较的结果是相等,但 alex 不是charles

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

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

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

在==和is之间选择

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

x is None

否定的写法

x is not None

元组的相对不可变性

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

>>> t1 = (1, 2, [30, 40])
>>> t2 = (1, 2, [30, 40])
>>> t1 == t2
True
>>> id(t1[-1])
>>> t1[-1].append(1000)
>>> t1
(1, 2, [30, 40, 1000])
>>> t1 == t2
False

表明,元组的值会随着引用的可变对象的变化而变。元组中不可变的是元素的标识。

默认做浅复制

复制列表(或多数内置的可变集合)最简单的方式是使用内置的类型构造方法。例如:

>>> l1 = [3, [55, 44], (7, 8, 9)]
>>> l2 = list(l1)
>>> l3 = l1[:]
>>> l2
[3, [55, 44], (7, 8, 9)]
>>> l3
[3, [55, 44], (7, 8, 9)]
>>> l1 == l2 == l3
True
>>> l2 is l1
False
>>> l3 is l1
False

为一个包含另一个列表的列表做浅复制;把这段代码复制粘贴到 Python Tutor (http://www.pythontutor.com)网站中,看看动画效果

l1 = [3, [66, 55, 44], (7, 8, 9)]
l2 = list(l1)    #浅复制了l1
l1.append(100)    #l1列表在尾部添加数值100
l1[1].remove(55)   #移除列表中第1个索引的值
print('l1:', l1)
print('l2:', l2)
l2[1] += [33, 22]   #l2列表中第1个索引做列表拼接
l2[2] += (10, 11)   #l2列表中的第2个索引做元祖拼接
print('l1:', l1)
print('l2:', l2)

l2 是 l1 的浅复制副本

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

为任意对象做深复制和浅复制

浅复制没什么问题,但有时我们需要的是深复制(即副本不共享内部对象的引用)。copy 模块提供的 deepcopy 和 copy 函数能为任意对象做深复制和浅复制。

 校车乘客在途中上车和下车

class Bus:

 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)

我们将创建一个 Bus 实例(bus1)和两个副本,一个是浅复制副本(bus2),另一个是深复制副本(bus3),看看在 bus1 有学生下车后会发生什么。

from copy import copy, deepcopy

bus1 = Bus(['Alice', 'Bill', 'Claire', 'David'])
bus2 = copy(bus1)      #bus2浅复制的bus1
bus3 = deepcopy(bus1)     #bus3深复制了bus1
print(id(bus1), id(bus2), id(bus3))  #查看三个对象的内存地址

bus1.drop('Bill')      #bus1的车上Bill下车了
print('bus2:', bus2.passengers)   #wtf....bus2中的Bill也没有了,见鬼了!
print(id(bus1.passengers), id(bus2.passengers), id(bus3.passengers)) #审查 passengers 属性后发现,bus1和bus2共享同一个列表对象,因为 bus2 是 bus1 的浅复制副本

print('bus3:', bus3.passengers)   #bus3是bus1 的深复制副本,因此它的 passengers 属性指代另一个列表

以上代码执行的结果为:

4324829840 4324830176 4324830736
bus2: ['Alice', 'Claire', 'David']
4324861256 4324861256 4324849608
bus3: ['Alice', 'Bill', 'Claire', 'David']

循环引用:b 引用 a,然后追加到 a 中;deepcopy 会想办法复制 a

>>> a = [10, 20]
>>> b = [a, 30]
>>> a.append(b)
>>> a
[10, 20, [[...], 30]]
>>> from copy import deepcopy
>>> c = deepcopy(a)
>>> c
[10, 20, [[...], 30]]

函数的参数作为引用时

Python 唯一支持的参数传递模式是共享传参(call by sharing)。多数面向对象语言都采用这一模式,包括 Ruby、Smalltalk 和 Java(Java 的引用类型是这样,基本类型按值传参)。共享传参指函数的各个形式参数获得实参中各个引用的副本。也就是说,函数内部的形参是实参的别名。

函数可能会修改接收到的任何可变对象

>>> def f(a, b):
...  a += b
...  return a
... 
>>> x = 1
>>> y = 2
>>> f(x, y)
>>> x, y
(1, 2)
>>> a = [1, 2]
>>> b = [3, 4]
>>> f(a, b)
[1, 2, 3, 4]
>>> a, b
([1, 2, 3, 4], [3, 4])
>>> t = (10, 20)
>>> u = (30, 40)
>>> f(t, u)
(10, 20, 30, 40)
>>> t, u
((10, 20), (30, 40))

数字x没有变化,列表a变了,元祖t没变化

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

可选参数可以有默认值,这是 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)


bus1 = HauntedBus(['Alice', 'Bill'])
print('bus1上的乘客:', bus1.passengers)
bus1.pick('Charlie')   #bus1上来一名乘客Charile
bus1.drop('Alice')    #bus1下去一名乘客Alice
print('bus1上的乘客:', bus1.passengers)   #打印bus1上的乘客

bus2 = HauntedBus()    #实例化bus2
bus2.pick('Carrie')    #bus2上来一名课程Carrie
print('bus2上的乘客:', bus2.passengers)

bus3 = HauntedBus()
print('bus3上的乘客:', bus3.passengers)
bus3.pick('Dave')
print('bus2上的乘客:', bus2.passengers)  #登录到bus3上的乘客Dava跑到了bus2上面

print('bus2是否为bus3的对象:', bus2.passengers is bus3.passengers)
print('bus1上的乘客:', bus1.passengers)

以上代码执行的结果为:

bus1上的乘客: ['Alice', 'Bill']
bus1上的乘客: ['Bill', 'Charlie']
bus2上的乘客: ['Carrie']
bus3上的乘客: ['Carrie']
bus2上的乘客: ['Carrie', 'Dave']
bus2是否为bus3的对象: True
bus1上的乘客: ['Bill', 'Charlie']

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

防御可变参数

如果定义的函数接收可变参数,应该谨慎考虑调用方是否期望修改传入的参数。

例如,如果函数接收一个字典,而且在处理的过程中要修改它,那么这个副作用要不要体现到函数外部?具体情况具体分析。这其实需要函数的编写者和调用方达成共识。

TwilightBus 实例与客户共享乘客列表,这会产生意料之外的结果。在分析实现之前,我们先从客户的角度看看 TwilightBus 类是如何工作的。

从 TwilightBus 下车后,乘客消失了

class TwilightBus:
 """让乘客销声匿迹的校车"""

 def __init__(self, passengers=None):
  if passengers is None:
   self.passengers = passengers
  else:
   self.passengers = passengers #这个地方就需要注意了,这里传递的是引用的别名

 def pick(self, name):
  self.passengers.append(name)  #会修改构造放的列表,也就是会修改外部的数据

 def drop(self, name):
  self.passengers.remove(name)  #会修改构造放的列表,也就是会修改外部的数据

basketball_team = ['Sue', 'Tina', 'Maya', 'Diana', 'Pat']
bus = TwilightBus(basketball_team)
bus.drop('Tina')  #bus中乘客Tina下去了
bus.drop('Pat')   #bus中课程Pat下去了

print(basketball_team) #wtf....为毛线的basketball的里面这两个人也木有了~~MMP

以上代码执行的结果为:

['Sue', 'Maya', 'Diana']

解决方案,不直接引用外部的basketball_team,而是在内部创建一个副本,类似于下面的这种

>>> a = [1, 2, 3]
>>> b = a
>>> c = list(a)
>>> b.append(10)
>>> a
[1, 2, 3, 10]
>>> b
[1, 2, 3, 10]
>>> c
[1, 2, 3]

c是a的副本,不会因为本身列表的变化而受影响,在上面的 ? 中,只需要在构造函数中创建一个副本即可(self.passengers=list(passengers))

del和垃圾回收

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

>>> import weakref
>>> s1 = {1, 2, 3}
>>> s2 = s1 #s1和s2是别名,指向同一个集合
>>> def bye(): #这个函数一定不能是要销毁的对象的绑定方法,否则会有一个指向对象的引用
...  print('Gone with the wind...')
... 
>>> ender = weakref.finalize(s1, bye) #在s1引用的对象上注册bye回调 
>>> ender.alive#调用finalize对象之前,.alive属性的值为True
True
>>> del s1 #del不删除对象,而是删除对象的引用
>>> ender.alive
True
>>> s2 = 'spam'  #重新绑定最后一个引用s2,让{1, 2, 3}无法获取,对象呗销毁了,调用bye回调,ender.alive的值编程了False
Gone with the wind...
>>> ender.alive
False

弱引用

正是因为有引用,对象才会在内存中存在。当对象的引用数量归零后,垃圾回收程序会把对象销毁。但是,有时需要引用对象,而不让对象存在的时间超过所需时间。这经常用在缓存中。

弱引用不会增加对象的引用数量。引用的目标对象称为所指对象(referent)。因此我们说,弱引用不会妨碍所指对象被当作垃圾回收。

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

>>> import weakref
>>> a_set = {0, 1}
>>> wref = weakref.ref(a_set)#创建弱引用对象wref,下一行审查它
>>> wref
<weakref at 0x101ce03b8; to 'set' at 0x101cd8d68>
>>> wref() #调用wref()返回的是被引用的对象,{0, 1}。因为这是控制台会话,所以{0, 1}会绑定给_变量
{0, 1}
>>> a_set = {2, 3, 4} #a_set不在指代{0, 1}集合,因此集合的引用数量减少了,但是_变量仍然指代它
>>> wref() #调用wref()已经返回了{0, 1}
{0, 1}
>>> wref() is None#计算这个表达式时,{0, 1}存在,因此wref()不是None,但是,随后_绑定到结果值False,现在{0,1}没有强引用
False
>>> wref() is None#因为{0, 1}对象不存在了,所以wref()返回了None
True

以上这篇基于Python对象引用、可变性和垃圾回收详解就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持三水点靠木。

Python 相关文章推荐
Python字符串格式化
Jun 15 Python
Python实现LRU算法的2种方法
Jun 24 Python
windows10下python3.5 pip3安装图文教程
Apr 02 Python
完美解决Pycharm无法导入包的问题 Unresolved reference
May 18 Python
python绘制中国大陆人口热力图
Nov 07 Python
pyinstaller打包多个py文件和去除cmd黑框的方法
Jun 21 Python
Python Django 实现简单注册功能过程详解
Jul 29 Python
Python切图九宫格的实现方法
Oct 10 Python
Python的几种主动结束程序方式
Nov 22 Python
Pycharm中如何关掉python console
Oct 27 Python
使用python操作lmdb对数据读取的实例
Dec 11 Python
Python趣味实战之手把手教你实现举牌小人生成器
Jun 07 Python
python 垃圾收集机制的实例详解
Aug 20 #Python
python 实现tar文件压缩解压的实例详解
Aug 20 #Python
详解Python 序列化Serialize 和 反序列化Deserialize
Aug 20 #Python
Python中input与raw_input 之间的比较
Aug 20 #Python
Python 基础教程之str和repr的详解
Aug 20 #Python
Python实现爬取需要登录的网站完整示例
Aug 19 #Python
Python获取当前页面内所有链接的四种方法对比分析
Aug 19 #Python
You might like
php cookie 登录验证示例代码
2009/03/16 PHP
php中日期加减法运算实现代码
2011/12/08 PHP
PHP网页游戏学习之Xnova(ogame)源码解读(五)
2014/06/23 PHP
PHP实现支持GET,POST,Multipart/form-data的HTTP请求类
2014/09/24 PHP
PHP代码维护,重构变困难的4种原因分析
2016/01/25 PHP
延时重复执行函数 lLoopRun.js
2007/05/08 Javascript
js加解密 脚本解密
2008/02/22 Javascript
根据当前时间在jsp页面上显示上午或下午
2014/08/18 Javascript
jQuery动画效果实现图片无缝连续滚动
2016/01/12 Javascript
JS取数字小数点后两位或n位的简单方法
2016/10/24 Javascript
js获取浏览器高度 窗口高度 元素尺寸 偏移属性的方法
2016/11/21 Javascript
AngularJS实现动态添加Option的方法
2017/05/17 Javascript
使用重写url机制实现验证码换一张功能
2017/08/01 Javascript
vue之浏览器存储方法封装实例
2018/03/15 Javascript
koa-router源码学习小结
2018/09/07 Javascript
JS与SQL方式随机生成高强度密码示例
2018/12/29 Javascript
Vue仿微信app页面跳转动画效果
2019/08/21 Javascript
安装多版本Vue-CLI的实现方法
2020/03/24 Javascript
JS错误处理与调试操作实例分析
2020/04/13 Javascript
Python实现扫描指定目录下的子目录及文件的方法
2014/07/16 Python
python通过邮件服务器端口发送邮件的方法
2015/04/30 Python
用python的requests第三方模块抓取王者荣耀所有英雄的皮肤实例
2017/12/14 Python
Python with语句上下文管理器两种实现方法分析
2018/02/09 Python
python自带tkinter库实现棋盘覆盖图形界面
2019/07/17 Python
python 利用jinja2模板生成html代码实例
2019/10/10 Python
Python3 pywin32模块安装的详细步骤
2020/05/26 Python
python xlsxwriter模块的使用
2020/12/24 Python
澳大利亚在线购买儿童玩具:Toy Universe
2017/12/28 全球购物
北美Newegg打造的全球尖货海购平台:tt海购
2018/09/28 全球购物
学员自我鉴定
2014/03/19 职场文书
生日礼品店创业计划书范文
2014/03/21 职场文书
办公室领导干部作风整顿个人整改措施
2014/09/17 职场文书
投资申请报告
2015/05/19 职场文书
小学同学聚会感言
2015/07/30 职场文书
Python3 如何开启自带http服务
2021/05/18 Python
python的html标准库
2022/04/29 Python