python循环计数_Python一切皆是对象,但这和内存管理有什么关系?

点击上方蓝字,和我一起学技术。

e4c1c556cc116f4b01073845029c5d88.png

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

Python的内存管理机制

对于工程师而言,内存管理机制非常重要,是绕不过去的一环。如果你是Java工程师,面试的时候一定会问JVM。C++工程师也一定会问内存泄漏,同样我们想要深入学习Python,内存管理机制也是绕不过去的一环。

不过好在Python的内存管理机制相对来说比较简单,我们也不用特别深入其中的细节,简单做个了解即可。

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

3f454c29c11c26cb076c37c492f08801.png

我们看到的是变量名,但是变量名指向了内存当中的一块对象。这种关系在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):passif __name__ == '__main__':
    a = Test()
    b = Test()
    a.t = b
    b.t = a

如果你打个断点来看的话,会看到a和b之间的循环引用:

91d4b9216f725e445f7eb739debab393.png

这里是无限展开的,因为这是一个无限循环。无限循环并不会导致程序崩溃, 也不会带来太大的问题,它的问题只有一个,就是根据前面介绍的引用计数法,a和b的引用永远不可能为0。

也就是说根据引用计数的原则,这两个变量永远不会被回收,这显然是不合理的。虽然Python当中专门建立了机制来解决引用循环的问题,但是我们并不知道它什么时候会被触发。

这个问题在Python当中非常普遍,尤其在我们实现一些数据结构的时候。举个最简单的例子就是树中的节点,就是引用循环的。因为父节点会存储所有的孩子,往往孩子节点也会存储父节点的信息。那么这就构成了引用循环。

class Node:def __init__(self, val, father):
    self.val = val
    self.father = father
    self.childs = []

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值