龙空技术网

Python编程:对象、类型和协议之引用计数、垃圾回收及引用与拷贝

传新视界 140

前言:

此刻兄弟们对“js计数引用”大致比较关怀,你们都需要剖析一些“js计数引用”的相关资讯。那么小编也在网上网罗了一些有关“js计数引用””的相关文章,希望各位老铁们能喜欢,你们快快来学习一下吧!

前言

在前篇文字中简要介绍了Python编程中对象相关的核心概念以及示例(详见"基本概念和对象标识" )。我们知道创建对象是要消耗内存和时间的。如果你高兴可以创建任意多的对象,直到没有可用内存为止。所以问题就来了:Python是如何管理所创建的的对象呢?这就是本篇内容所要关注的重点。我尽量用简单的文字和示例进行介绍,以便一看就懂。

引用计数和垃圾回收

简而言之,Python是通过自动垃圾回收来管理对象的。Python对所有的对象进行引用计数(reference-counted)。每当对象被赋给一个新变量名称或被放入一个容器中,如列表、元组或字典中,它的引用计数就会增加,如下所示:

a = 37 # 创建一个值为37的对象

b = a # 在37上增加引用计数

c = []

c.append(b) # 再次增加对37的计数引用

上面代码示例创建了一个包含值为37的对象。a是初始引用新创建对象的名称。当b被赋值为a时, b成为同一对象的新名称,对象的引用计数增加。同样,当将b放入列表中时,对象的引用计数再次增加。在整个示例中,只有一个对应于37的对象. 所有其他操作都在创建对该对象的引用。

对象的引用计数的减少是通过del语句实现,或者当引用超出作用域(或被重新分配)时减少。简单示例如下:

del a # 减少37的引用计数

b = 42 # 减少37的引用计数——不再引用a或37

c[0] = 3.0 #原来c[0]为37,通过重新赋值而减少37的引用计数

对象的当前引用计数可以使用sys.getrefcount()函数获得。示例如下:

>>> a = 34

>>> a = 37

>>> import sys

>>> sys.getrefcount(a)

11

>>>

由于系统环境原因,你的引用结果可能与这里显示的不同,不必介意,这是正常现象。换句话说,引用计数通常比您猜测的要高得多。对于数字和字符串等不可变数据,解释器会在程序的不同部分之间积极地共享对象,以节省内存。你只是没有注意到,因为对象是不可变的。

当对象的引用计数达到零时,它将被垃圾回收。然而,在某些情况下,循环依赖关系可能存在于不再使用的对象集合中。看下面的例子:

a = { } #字典对象

b = { } #字典对象

a['b'] = b # a包含对b的引用

b['a'] = a # b包含对a的引用

del a

del b

在本例中,del语句减少了a和b的引用计数,并销毁了用于引用底层对象的名称。但是,因为每个对象都包含对另一个对象的引用,所以引用计数不会降为零,对象仍然保留内存分配。最终,解释器不会释放内存,因而对象的销毁会被延迟,直到执行一个循环检测器来查找和删除不可访问的对象。程序运行后,当解释器在执行过程中会分配越来越多的内存时,循环检测算法会周期性地运行。具体的行为可以使用gc标准库模块中的函数进行微调和控制。gc.collect()函数可用于立即调用循环垃圾收集器。

在大多数程序中,垃圾收集很简单地就“发生了”,你不需要去多想。然而,在某些情况下,手动删除对象可能是有意义的。在处理庞大的数据结构时,就会出现这样的场景。例如,来想想以下代码:

def some_calculation():

data = create_giant_data_structure()

#在计算的某些部分使用data数据

#释放数据data

del data

#继续计算

在这段代码中,del data语句的使用表明不再需要data变量。如果这导致引用计数达到0,则在该点立即对对象进行垃圾回收。如果没有del语句,对象将持续存在一段不确定的时间,直到数据变量在函数结束时超出作用域。这种情况,只有在试图弄清楚为什么程序使用了比应该或预想使用的更多的内存时,你才会注意到这一点。

引用和复制

当一个程序进行赋值时,例如b = a,这就创建一个对a的新引用。对于不可变对象,如数字和字符串,此赋值看起来似乎创建了a的副本(尽管事实并非如此)。然而,对于诸如列表和字典之类的可变对象,这种行为就显得非常不同了。这里有一个例子:

>>> a = [1,2,3,4]

>>> b = a #b是对a的引用

>>> b is a

True

>>> b[2] = -100 # 改变b中的一个元素值

>>> a # 注意,a的结果也被改变了

[1, 2, -100, 4]

>>>

因为a和b在本例中引用同一个对象,对其中一个变量所做的更改会反映在另一个变量中。为了避免这种情况,必须创建对象的副本/拷贝(复制一份),而不是创建新的引用。

有两种类型的拷贝操作可应用于诸如列表和字典之类的容器对象:浅复制(shallow copy)和深复制(deep copy)。

浅拷贝创建一个新对象,但用对原始对象中包含的相同项的引用填充它。示例如下:

>>> a = [ 1, 2, [3,4] ]

>>> b = list(a) #创建一个对a的浅复制

>>> b is a

False

>>> b.append(100) # 向b添加一个元素

>>> b

[1, 2, [3, 4], 100]

>>> a # 此时a没有任何改变

[1, 2, [3, 4]]

>>> b[2][0] = -100 # 改变b内的一个元素

>>> b

[1, 2, [-100, 4], 100]

>>> a #注意a内部的改变

[1, 2, [-100, 4]]

>>>

在本例中,a和b是单独的列表对象,但它们包含的元素是共享的。因此,对a的一个元素的修改也会修改b的一个元素,如上所示。

深度复制创建一个新对象并递归复制其中包含的所有对象。没有内置操作符来创建对象的深度复制副本。但是,可以使用标准库中的copy.deepcopy()函数来实现,如下所示:

>>> import copy

>>> a = [1, 2, [3, 4]]

>>> b = copy.deepcopy(a)

>>> b[2][0] = -100

>>> b

[1, 2, [-100, 4]]

>>> a # 注意:这里的a不受影响——a和b不再共享对象引用

[1, 2, [3, 4]]

>>>

在大多数程序中都不鼓励使用deepcopy()。复制对象很慢,而且通常没有必要。 保留deepcopy()是用于你知道实际上需要一个副本的情况下,以便更改数据,且不希望更改影响原始对象。另外,要注意deepcopy()函数对于涉及系统或运行时状态的对象(例如,打开的文件、网络连接、线程、生成器等)将失败,这是不适合的复制场景。

简单小结

本篇主要简单扼要地介绍了对象的引用计数、如何垃圾回收以及引用和副本复制形式(浅复制和深复制)的基本情况。

本篇就到这里了,点个赞,分分享取出吧,谢谢。欢迎沟通留言,再见。

标签: #js计数引用 #引用计数算法是一种常用的垃圾回收算法