基于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 相关文章推荐
详解Django之auth模块(用户认证)
Apr 17 Python
浅谈Series和DataFrame中的sort_index方法
Jun 07 Python
实例讲解Python爬取网页数据
Jul 08 Python
Windows下Anaconda2安装NLTK教程
Sep 19 Python
python控制nao机器人身体动作实例详解
Apr 29 Python
python微信撤回监测代码
Apr 29 Python
Python基础学习之函数方法实例详解
Jun 18 Python
python使用Qt界面以及逻辑实现方法
Jul 10 Python
python3 map函数和filter函数详解
Aug 26 Python
Python 面向对象之封装、继承、多态操作实例分析
Nov 21 Python
python 决策树算法的实现
Oct 09 Python
python3字符串输出常见面试题总结
Dec 01 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
在WAMP环境下搭建ZendDebugger php调试工具的方法
2011/07/18 PHP
PHP与MongoDB简介|安全|M+PHP应用实例详解
2013/06/17 PHP
php中base64_decode与base64_encode加密解密函数实例
2014/11/24 PHP
PHP编写RESTful接口
2016/02/23 PHP
Zend Framework教程之Autoloading用法详解
2016/03/08 PHP
laravel框架路由分组,中间件,命名空间,子域名,路由前缀实例分析
2020/02/18 PHP
php操作redis数据库常见方法实例总结
2020/02/20 PHP
Thinkphp 框架扩展之行为扩展原理与实现方法分析
2020/04/23 PHP
jQuery的写法不同导致的兼容性问题的解决方法
2010/07/29 Javascript
jquery maxlength使用说明
2011/09/09 Javascript
jquery如何把参数列严格转换成数组实现思路
2013/04/01 Javascript
JS来动态的修改url实现对url的增删查改
2014/09/05 Javascript
jquery操作 iframe的方法
2014/12/03 Javascript
使用javascript获取页面名称
2014/12/23 Javascript
jQuery中animate()方法用法实例
2014/12/24 Javascript
浅谈JavaScript中的对象及Promise对象的实现
2015/11/15 Javascript
jquery删除table当前行的实例代码
2016/10/07 Javascript
BootStrap实现带关闭按钮功能
2017/02/15 Javascript
vue系列之动态路由详解【原创】
2017/09/10 Javascript
React中常见的动画实现的几种方式
2018/01/10 Javascript
NodeJs项目中关闭ESLint的方法
2018/08/09 NodeJs
JavaScript定时器设置、使用与倒计时案例详解
2019/07/08 Javascript
深入理解Django的自定义过滤器
2017/10/17 Python
python将txt文件读取为字典的示例
2018/12/22 Python
用sqlalchemy构建Django连接池的实例
2019/08/29 Python
django 做 migrate 时 表已存在的处理方法
2019/08/31 Python
python opencv圆、椭圆与任意多边形的绘制实例详解
2020/02/06 Python
Python新手如何理解循环加载模块
2020/05/29 Python
可打印的优惠券、杂货和优惠券代码:Coupons.com
2018/06/12 全球购物
一份报关员的职业规划范文
2014/01/08 职场文书
上级检查欢迎词
2014/01/18 职场文书
先进工作者获奖感言
2014/02/08 职场文书
如何写自我评价?自我评价写什么好?
2014/03/14 职场文书
python基础入门之普通操作与函数(三)
2021/06/13 Python
FP-growth算法发现频繁项集——发现频繁项集
2021/06/24 Python
Nginx配置https的实现
2021/11/27 Servers