Python一切皆是对象,但这和内存管理有什么关系?

本文始发于我的公众号:TechFlow,原创不易,求个关注node


今天是Python的第15篇文章,咱们来聊聊Python中内存管理机制,以及循环引用的问题。web

Python的内存管理机制

对于工程师而言,内存管理机制很是重要,是绕不过去的一环。若是你是Java工程师,面试的时候必定会问JVM。C++工程师也必定会问内存泄漏,一样咱们想要深刻学习Python,内存管理机制也是绕不过去的一环。面试

不过好在Python的内存管理机制相对来讲比较简单,咱们也不用特别深刻其中的细节,简单作个了解便可。数据结构

Python内存管理机制的核心就是引用计数,在Python当中一切都是对象,对象经过引用来使用。app

咱们看到的是变量名,可是变量名指向了内存当中的一块对象。这种关系在Python当中称为引用,咱们经过引用来操做对象。因此根据这点,引用计数很好理解,也就是说咱们会对每个对象进行统计全部指向它的指针的数量。若是一个对象引用计数为0,那么说明它没有任何引用指向它,也就是说它已经没有在使用了,这个时候,Python就会将这块内存收回。编辑器

简单来讲引用计数原理就是这些,但咱们稍微深刻一点,来简单看看哪些场景会引发对象引用的变化。函数

引用计数的变化显然只有两种,一种是增长,一种是减小,这两种场景都只有4种状况。咱们先来看下增长的状况:性能

  1. 首先是初始化,最简单的就是咱们用 赋值操做给一个变量赋值。举个例子:
n = 123
复制代码

这就是最简单的初始化操做,虽然123在咱们来看是一个常数,可是在Python底层一样被认为是一个常数对象。n是它的一个引用。学习

  1. 第二种状况是引用的传递,最简单的就是咱们将一个变量的值赋值给了另一个变量。
m = n
复制代码

好比咱们将n赋值给m,它的本质是咱们建立了一个新的引用,指向了一样一块内存。若是咱们用id操做去查看m和n的id,会发现它们的id是同样的。也就是说它们并非存储了两份相同的值,而是指向了同一份值。并非有两个叫作王小二的人,而是王小二有两个不一样的帐号测试

  1. 第三种状况是做为元素被存储进了容器当中,好比被存储进了list当中。
a = [1, 2, 123]
复制代码

虽然咱们用到了一个容器,可是容器并不会拷贝一份这些对象,仍是只是存储这些对象的引用

  1. 最后一种状况就是做为参数传给函数,在Python当中,全部的传参都是引用传递。这也是为何,咱们常常看到有人会这样写代码的缘由:
def test(a):
 a.append(3)  a = [] test(a) print(a) 复制代码

咱们根据上面列举的这四种引用计数增长的状况,不难推导出引用减小的状况, 其实基本上是对称的操做

  1. 和初始化对应的操做是 销毁,好比咱们建立的对象被del操做给销毁了,那么一样引用计数会-1
del n
复制代码
  1. 和赋值给其余变量名的操做相反的操做是 覆盖,好比以前咱们的n=123,也就是n这个变量指向123,如今咱们将n赋值成其余值,那么123这个对象的引用计数一样会减小。
n = 124
复制代码
  1. 既然元素存储在容器当中会带来引用计数,那么一样元素 从容器当中移除也会减小引用计数。这个也很好理解,最简单的就是list调用remove方法移除一个元素:
a.remove(123)
复制代码
  1. 最后一个对应的就是做用域,也就是当变量 离开了做用域,那么它对应的内存块的引用计数一样会减小。好比咱们函数调用结束,那么做为参数的这些变量对应的引用计数都会减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就颇有必要了。

今天的文章就到这里,原创不易,扫码关注我,获取更多精彩文章。

相关文章
相关标签/搜索