Python一切皆是对象,但这和内存管理有什么关系?
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是Python的第15篇文章,我们来聊聊Python中内存管理机制,以及循环引用的问题。
Python的内存管理机制
对于工程师而言,内存管理机制非常重要,是绕不过去的一环。如果你是Java工程师,面试的时候一定会问JVM。C++工程师也一定会问内存泄漏,同样我们想要深入学习Python,内存管理机制也是绕不过去的一环。
不过好在Python的内存管理机制相对来说比较简单,我们也不用特别深入其中的细节,简单做个了解即可。
Python内存管理机制的核心就是引用计数,在Python当中一切都是对象,对象通过引用来使用。
我们看到的是变量名,但是变量名指向了内存当中的一块对象。这种关系在Python当中称为引用,我们通过引用来操作对象。所以根据这点,引用计数很好理解,也就是说我们会对每一个对象进行统计所有指向它的指针的数量。如果一个对象引用计数为0,那么说明它没有任何引用指向它,也就是说它已经没有在使用了,这个时候,Python就会将这块内存收回。
简单来说引用计数原理就是这些,但我们稍微深入一点,来简单看看哪些场景会引起对象引用的变化。
引用计数的变化显然只有两种,一种是增加,一种是减少,这两种场景都只有4种情况。我们先来看下增加的情况:
首先是初始化,最简单的就是我们用赋值操作给一个变量赋值。举个例子:
n = 123
这就是最简单的初始化操作,虽然123在我们来看是一个常数,但是在Python底层同样被认为是一个常数对象。n是它的一个引用。
第二种情况是引用的传递,最简单的就是我们将一个变量的值赋值给了另外一个变量。
m = n
比如我们将n赋值给m,它的本质是我们创建了一个新的引用,指向了同样一块内存。如果我们用id操作去查看m和n的id,会发现它们的id是一样的。也就是说它们并不是存储了两份相同的值,而是指向了同一份值。并不是有两个叫做王小二的人,而是王小二有两个不同的账号。
第三种情况是作为元素被存储进了容器当中,比如被存储进了list当中。
a = [1, 2, 123]
虽然我们用到了一个容器,但是容器并不会拷贝一份这些对象,还是只是存储这些对象的引用。
最后一种情况就是作为参数传给函数,在Python当中,所有的传参都是引用传递。这也是为什么,我们经常看到有人会这样写代码的原因:
def test(a):
a.append(3)
a = []
test(a)
print(a)
我们根据上面列举的这四种引用计数增加的情况,不难推导出引用减少的情况, 其实基本上是对称的操作。
和初始化对应的操作是销毁,比如我们创建的对象被del操作给销毁了,那么同样引用计数会-1
del n
和赋值给其他变量名的操作相反的操作是覆盖,比如之前我们的n=123,也就是n这个变量指向123,现在我们将n赋值成其他值,那么123这个对象的引用计数同样会减少。
n = 124
既然元素存储在容器当中会带来引用计数,那么同样元素从容器当中移除也会减少引用计数。这个也很好理解,最简单的就是list调用remove方法移除一个元素:
a.remove(123)
最后一个对应的就是作用域,也就是当变量离开了作用域,那么它对应的内存块的引用计数同样会减少。比如我们函数调用结束,那么作为参数的这些变量对应的引用计数都会减1。
如果一个对象的引用计数减到0,也就是没有引用再指向它的时候,那么当Python进行gc的时候,这块内存就会被释放,也就是这个对象会被清除,腾出空间来。
注意一下,引用计数减到0与内存回收之间并不是立即发生的,而是有一段间隔的。根据Python的机制,内存回收只会在特定条件下执行。在占用内存比较小还有很多富裕的情况下,往往是不会执行内存回收的。因为Python在执行gc(garbage collection)的时候也会stop the world,也就是暂停其他所有的任务,所以这是影响性能的一件事情,只会在有必要的时候执行。
我们费这么大劲来介绍Python中的内存机制,除了向大家科普一下这一块内容之外,更重要的一点是为了引出我们开发的时候经常遇见的一种情况——循环引用。
循环引用
如果熟悉了Python的引用,来理解循环引用是非常容易的。说白了也很简单,就是你的一个变量引用我,我的一个变量引用你。
我们来写一段简单的代码,来看看循环引用:
class Test:
def __init__(self):
pass
if __name__ == '__main__':
a = Test()
b = Test()
a.t = b
b.t = a
如果你打个断点来看的话,会看到a和b之间的循环引用:
这里是无限展开的,因为这是一个无限循环。无限循环并不会导致程序崩溃, 也不会带来太大的问题,它的问题只有一个,就是根据前面介绍的引用计数法,a和b的引用永远不可能为0。
也就是说根据引用计数的原则,这两个变量永远不会被回收,这显然是不合理的。虽然Python当中专门建立了机制来解决引用循环的问题,但是我们并不知道它什么时候会被触发。
这个问题在Python当中非常普遍,尤其在我们实现一些数据结构的时候。举个最简单的例子就是树中的节点,就是引用循环的。因为父节点会存储所有的孩子,往往孩子节点也会存储父节点的信息。那么这就构成了引用循环。
class Node:
def __init__(self, val, father):
self.val = val
self.father = father
self.childs = []
弱引用
为了解决这个问题,Python中提供了一个叫做弱引用的概念。弱引用本质也是一种引用,但是它不会增加对象的引用计数。也就是说它不能保证它引用的对象一定不会被销毁,只要没有销毁,弱引用就可以返回预期的结果。
弱引用不用我们自己开发,这是Python当中集成的一个现成的模块weakref。
这个模块当中的方法很多,用法也很多,但是我们基本上用不到,一般来说最常用的就是ref方法。通过weakref库中的ref方法,可以返回对象的一个弱引用。我们还是来看个例子:
import weakref
class Test:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
if __name__ == '__main__':
a = Test('a')
b = Test('b')
a.t = weakref.ref(b)
b.t = weakref.ref(a)
print(a.t())
其实还是之前的代码,只是做了一点简单的改动。一个是我们给Test加上了name这个属性,以及str方法。另一个是我们把直接赋值改成了使用weakref。
这一次我们再打断点进来看的话,就看不到无限循环的情况了:
ref返回的是一个获取引用对象的方法,而不是对象本身。所以我们想要获取这个对象的话,需要再把它当成函数调用一下。
当然这样很麻烦,我们还有更好的办法,就是使用property注解。通过property注解,我们可以把weakref封装掉,这样在使用的时候就没有感知了。
import weakref
class Test:
def __init__(self, name):
self.name = name
def __str__(self):
return self.name
@property
def node(self):
return None if self._node is None else self._node()
@node.setter
def node(self, node):
self._node = weakref.ref(node)
总结
引用和循环引用都是基于Python本身的机制,如果对这块机制不了解,很容易采坑。因为可能会出现逻辑是对的,但是有一些意想不到的bug的情况。这种时候,往往很难通过review代码或者是测试发现,这也是我们学习的瓶颈所在。很容易发现代码已经写得很熟练了,但是一些进阶的代码还是看不懂或者是写不出来,本质上就是因为缺少了对于底层的了解和认知。
循环引用的问题在我们开发代码的时候还蛮常见的,尤其是涉及到树和图的数据结构的时候。由于循环引用的关系,很有可能出现被删除的树仍然占用着空间,内存不足的情况发生。这个时候使用weakref就很有必要了。
今天的文章就到这里,原创不易,扫码关注我,获取更多精彩文章。
Python一切皆是对象,但这和内存管理有什么关系?的更多相关文章
- Python 源码剖析(六)【内存管理机制】
六.内存管理机制 1.内存管理架构 2.小块空间的内存池 3.循环引用的垃圾收集 4.python中的垃圾收集 1.内存管理架构 Python内存管理机制有两套实现,由编译符号PYMALLOC_DEB ...
- Java对象和它的内存管理
java中的内存管理分为两个方面: 内存分配:指创建java对象时JVM为该对象在堆空间中所分配的内存空间. 内存回收:指java 对象失去引用,变成垃圾时,JVM的垃圾回收机制自动清理该对象,并回收 ...
- Objective-C MRC多个对象相互引用的内存管理
在MRC环境下,假定CTRoom对象是CTPerson的一个成员变量,那么修改CTRoom对象时应注意,代码如下: - (void) setRoom:(CTRoom *) room { //需判断新旧 ...
- Python深入之python内存管理机制(重点)
前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者:醍醐三叶 关于python的存储问题, (1)由于python中 ...
- (重点)Python深入之Python内存管理机制你会吗?
前言本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理.作者:醍醐三叶 请注意:如果你平时学Python遇到问题找不到人解答?或者没有 ...
- python的内存管理机制
先从较浅的层面来说,Python的内存管理机制可以从三个方面来讲 (1)垃圾回收 (2)引用计数 (3)内存池机制 一.垃圾回收: python不像C++,Java等语言一样,他们可以不用事先声明变量 ...
- Python 源码学习之内存管理 -- (转)
Python 的内存管理架构(Objects/obmalloc.c): _____ ______ ______ ________ [ int ] [ dict ] [ list ] ... [ str ...
- python的内存管理机制(zz)
本文转载自:http://www.cnblogs.com/CBDoctor/p/3781078.html 先从较浅的层面来说,Python的内存管理机制可以从三个方面来讲 (1)垃圾回收 (2)引用计 ...
- python 进阶读书笔记1 -- 理解python一切皆对象
理解python一切皆对象: 1.所有的类都是由type创建的 2.所有的类的基类都是object 3.type是类,也是实例,type的基类是object,type对象是由type创建的 4.obj ...
随机推荐
- thinkphp 5 一些常见问题
## 请求缓存 request_cache
- LVS+Keepalived 实现高可用负载均衡集群
LVS+Keepalived 实现高可用负载均衡集群 随着网站业务量的增长,网站的服务器压力越来越大?需要负载均衡方案!商业的硬件如 F5 ,Array又太贵,你们又是创业型互联公司如何有效 ...
- mysql之浅谈主外键
主键(PRIMARY KEY) 主键在一个数据表中只能有唯一的一个,约束当前字段的值不能重复,且非空保证数据的完整性,也可以当做当前数据表的标识符用来查询(当做索引,唯一性索引的一种) 创建带主键的表 ...
- 小米Note 10 Lite海外发布 无缘中国市场
[TechWeb]5月1日消息,昨日晚间,小米Note 10 Lite在海外亮相.小米市场部副总经理臧智渊在微博透露,小米Note 10 Lite 6GB+64GB版售价349欧元(约合人民币2700 ...
- 工程师泄露5G核心技术文档 被判有期徒刑三年缓刑四年
2002 年至 2017 年 1 月,黄某瑜就职于中兴通讯公司,担任过射频工程师.无线架构师等职务.2008 年 4 月至 2016 年 10 月,王某就职于中兴通讯公司西安研究所,担任过 RRU 部 ...
- flutter在2019年会有怎样的表现?
2019独角兽企业重金招聘Python工程师标准>>> Flutter的趋势 在移动端,受成本和效率的驱使,跨平台一站式开发慢慢成为一个趋势.从Hybird,RN,WEEX,Flut ...
- Micropython教程之TPYBoard开发板驱动舵机教程(萝卜学科编程教育)
大家应该都看到过机器人的手臂啊腿脚啊什么的一抽一抽的在动弹吧...是不是和机械舞一样的有节奏,现在很多机器人模型里面的动力器件都是舵机. 但是大家一般见到的动力器件都是像步进电机,直流电机这一类的动力 ...
- 蚂蚁金服合作的RISE实验室到底有多牛?
近日,蚂蚁金服与美国加州伯克利大学近期新成立的RISE实验室达成合作意向.RISE实验室的前身是著名伯克利AMP实验室,主导研发了当今大数据计算领域最前沿的开源系统:Apache Spark.Apac ...
- mysql基础(三)存储引擎和锁
存储引擎的概念: 关系型数据库表是用于存储和组织信息的数据结构,可以将表理解为由行和列组成的表格,各种各样,不同的表结构意味着存储不同类型的数据,在数据的处理上也会存在着差异,对于mysql来说,它提 ...
- Mbatis逆向工程常遇错误
org.apache.ibatis.exceptions.PersistenceException: ### Error building SqlSession.### The error may e ...