详解Python垃圾回收机制和常量池的验证

Python的引入

人类认识世界是从认识世界中的一个又一个实物开始,然后再对其用语言加以描述。例如当中国人看到苹果时,便会用中文“苹果”加以描述,而用英语的一些国家则会用“apple”加以描述。

以上说到的中文和英文都是人类认识并描述世界的一个工具,而在计算机的世界中,为了让计算机去认知世界,从而帮助人类完成更多的任务。在计算机领域中也发展了语言这个工具,从早期的机器语言到汇编语言再到现在使用范围较广的高级语言。而我们接下来要介绍的Python则属于高级语言这一分支。

变量的引入

为什么要有变量

上面说到Python是计算机世界中用来描述外部世界的,并且也提及了世界就是一个又一个实物的堆叠,描述世界其实就是去描述那一个又一个实物,人类如此,计算机也是如此。因此计算机语言开发者们为了使用计算机语言的人更好的在计算机中去描述这些实物,便在计算机语言中引入了变量这个概念,Python也不例外。简单点说,变量就是用来描述世间万物的。

定义变量

为了在计算机书写方便,定义一变量也有一定的规则,在这里我们仅说说Python中变量的定义规则,首先我们先定义两个变量:

name = 'chenyoude'
year = 2021

上述代码中我们便定义了两个变量,从上面定义的两个变量中,我们可以看到,变量的组成分为三个部分:

  1. 变量名:反应变量值所描述的意义,并且可以用来引用变量值。
  2. 赋值符号:赋值。
  3. 变量值:存放数据,用来记录现实世界中的某种状态。

常量引入

上面简单讲解了Python中的变量,通过字面意思,可以看到变量其实是一个变化的量,例如,下面这个实例:

year = 2021
year = year + 1
print(year) # 输出结果:2022

刚开始我们赋予了year一个变量值为2021,当我们对year进行加1操作时,可以发现year值变成了2022。对于上述现象我们不难理解,因为之前说过Python中变量是用来描述世间万物的,世间万物在现实中是可以变化的,变量当然也可以随之变化。

但是在某个局部范围内,变量可能是不会变化的,例如在2021年这一年,都只会是2021年,没有人会说2021年是2022年。如果你有丰富的开发经验,会明白变量定义出来不是存放在那里给你看的,更多的是要拿来用的。也就是说如果在2021年中的某个程序需要使用year这个变量,但这个变量是不需要进行修改的。为了防止误操作对year这个变量进行了修改,计算机语言便设计了常量这个概念,也就是说常量相对于变量是一个不会变化的量。

在Python中,有没有常量呢?不严格的讲,其实是有的,只是在定义常量的时候常量名必须的全大写,例如,下面这个实例:

YEAR = 2021
YEAR = YEAR + 1
print(YEAR) # 输出结果:2022

上面这个常量的实例令人大吃一惊,因为使用常量YEAR后和使用变量year的结果一致,也就是说常量YEAR遭到了更改。但是,稍微解释你就明白了。

在Python中,虽然也和其他很多计算机语言一样拥有常量这个概念,但更多的是约定俗成的,Python并没有严格的对常量进行控制,只是规定常量名必须全部大写。原因很简单:都是常量了,你为什么还要修改?

常量池引入

上面讲到常量就是一个不会变化的变量,严格的讲,在Python中是没有常量这个概念的。但是,在Python中又有另外一种例外,那就是常量池,为了搞清楚常量池,首先我们得弄明白Python的几个小知识,接下来一一叙说。

Python解释器

上面提及到Python是计算机用来描述世间万物的一种语言,由于计算机没有人脑那么强大,计算机更多的只是认识高低压电频,再通过对高低压电频的转化进而编码成我们看到的一个又一个字符,也就是说计算机是无法直接认识利用Python写下的字符的。(此处设计计算机组成原理,不多做介绍)

也就是说,当我们利用Python写下一个又一个字符并且交给电脑时,需要通过编码这个过程,而这个编码的过程有时候也被称为解释。解释的原理就相当于从中文转成英文,只不过此时不是需要让英文使用者看懂中文,而是让计算机能够看懂Python。

中文转成英文的时候,可能需要一个翻译员或一个翻译软件,利用Python写下的字符转化为计算机能看懂的语言同样如此,这个转化过程也需要一个外物的帮助——Python解释器。

Python变量存储机制

假设我们使用Python解释器定义了以下一个变量:

year = 2021

当我们通过字符定义变量时,一定会好奇这些变量被Python解释器解释后到底去了哪?如果对计算机的组成熟悉的同学,一定会清楚计算机的核心组件为:CPU、内存、外存、输入设备、输出设备。也就是说,这些字符应该存储在这些核心组件中。在这里就不卖关子了,当我们通过字符定义变量并对其用Python解释器进行解释时,他们会以计算机能看懂的形式进入内存当中。

上面讲的对于很多非科班出身的朋友可能很难理解,在这里将它生动化。现在假设江西师范大学相当于电脑内存,每当有一批新学生进入师大时,师大都会开辟出一个新教室给这批新同学使用,并且会给每一个教室一个独一无二的教室牌号。由于把师大看作是内存,这批新同学就可以看成是变量值,而教室牌号就是变量名。也就是说,对于师大这个大内存,每定义一个变量year=2021,就会在这个大内存中开辟一个小空间,小空间中放变量值2021,然后大内存会给这个小空间定义一个变量名year,此时变量名year指向变量值2021。

上面说到每当Python解释器解释一个变量时,会将这个变量存放到内存中的一个小空间中,但如何知道这个小空间的具体位置呢?此处介绍Python的一个内置函数id(),通过这个函数可以获取某一个变量所在的内存地址,例如下面这个实例:

year = 2021
print(id(year)) # 输出4499932432

Python垃圾回收机制

对于上述师大的例子,此处再做延伸。由于那一批学生所在班级新转来了几位同学,需要那一批学生更换更大一点教室,也就是给他们一个新的教室。那么学校应该会这样处理,首先开辟一个新的教室,然后拿下那一批学生原有教室的教室牌号更换到这个新教室,最后会清空原有教室。

在Python中,也是如此,如果到了新的一年,我们会重新定义一个year变量,也就是year=2022。如果这是在同一个程序中如此做,Python会沿用上述更换教室的方法,它首先会解除year和2021的连接,开辟一个新内存存放变量值2022,让year与2022连接。此时,会发现2021这个变量值只有变量值而没有变量名,因此这个没有变量名的变量值会变成Python眼中的一个垃圾变量,从而触发Python垃圾回收机制,对这个2021所在的内存空间进行回收。

为了更好地理解Python垃圾回收机制,可以看下面这个例子:

year = 2021
print(id(year)) # 输出4499932720
print(year) # 输出2021

year = 2022
print(id(year)) # 输出4499932560
print(year) # 输出2022

通过上述例子,可以看到当新定义了一个year变量时,year会与新的变量进行一个连接。当然,此处所说的垃圾回收机制只是为了引入引用计数这个概念,并不是完全正确的解释,并且上述实例还无法证明变量值2021所在内存是否被回收,下面将通过引用计数的实例会进一步说明并重新解释垃圾回收机制。

引用计数

上面讲到如果某个变量值绑定着变量名,就是一个正常的变量,如果该变量值没有绑定着门牌号,这个变量就是一个垃圾变量,对于垃圾变量,Python会触发垃圾回收机制回收这个变量所占有的内存。进而可以想到,Python中一个变量名一定只能对应一个变量值。

在这里我们就不能沿用师大这个例子了,而得引出一个新的名词——引用计数。

为了解释引用计数,我们首先得明白在Python中,当定义了一个变量值为2021的变量时,它可以表示年份、也可以表示山的高度…也就是说一个变量名只能对应一个变量值,但是一个变量值可以对应不同的变量名,这种设计也是比较合理的。

现在我们引出引用计数这个概念,当相同的变量值被赋予不同的变量名时,变量值每增加一个变量名的赋予,则该变量值的引用计数加1。由于我们可以通过Python内置sys模块中的getrefcount()函数获取某一个变量的引用计数(getrefcount输出值默认从3开始),可以通过下面这个例子感受下:

import sys

# 引用计数初始值为3
print(sys.getrefcount(2021)) # 输出为3

year = 2021
print(sys.getrefcount(2021)) # 输出为4

height = 2021
print(sys.getrefcount(2021)) # 输出为5

del year
print(sys.getrefcount(2021)) # 输出为4

从上述代码可以看出变量值2021的引用计数由于每一次赋予新的变量名,引用计数都会增加,而当我们利用del关键字删除变量值2021的一个变量名year时,引用计数则会减少。

为了更加严谨的表达引用计数,此处不得不再次深入,引用计数字面意思可以理解为引用的次数,也就是说上面的例子其实并不严谨,更严谨的讲,只有当一个变量值每一次被直接或间接引用时,引用计数才会增加,在Python中让引用计数增加共有三种方法:

  1. 变量被创建,变量值引用计数加1
  2. 变量被引用,变量值引用计数加1
  3. 变量作为参数传入到一个函数,变量值引用计数加2

具体看下述实例:

import sys

# 引用计数初始值为3
print(sys.getrefcount(2021)) # 输出为3

# 变量被创建,变量值引用计数加1
year = 2021
print(sys.getrefcount(2021)) # 输出为4

# 变量被引用,变量值引用计数加1
height = year
print(sys.getrefcount(2021)) # 输出为5

# 变量作为参数传入到一个函数,变量值引用计数加2
def func(year):
  print(sys.getrefcount(year)) 

func(year) # 输出为7

Python中既然有增加引用计数的方法, 也当然会减少引用计数的方法,共有以下4种:

  1. 变量值对应的变量名被销毁
  2. 变量值对应的变量名被赋予新的值
  3. 变量值对应的变量名离开它的作用域
  4. 变量值对应的变量名的容器被销毁

重看Python垃圾回收机制

有了getrefcount()方法并通过引用计数,我们就可以解开垃圾回收机制遗留的一个问题——如何判断是否触发了垃圾回收机制。每当一个变量定义,他的getrefcount输出值为3,而如果该变量值被垃圾回收机制回收,则它的getrefcount输出值回到3,可以通过下面实例验证上述猜想:

import sys

print(sys.getrefcount(2021)) # 输出为3

year = 2021
print(sys.getrefcount(2021)) # 输出为4
print(id(year)) # 输出4499932720
print(year) # 输出2021

year = 2022
print(sys.getrefcount(2021)) # 输出为3
print(id(year)) # 输出4499932560
print(year) # 输出2022

通过上述实例,可以发现由于变量值2021对应的变量名被新的变量值2022引用,它的getrefcount输出值为3,引用计数变成了0,因此可以证明Python触发了垃圾回收机制。

如果对上述验证Python触发垃圾回收机制的实例深入挖掘,会发现当把year赋给变量值2022时,变量值的2021的引用计数为0,此时触发了Python的垃圾回收机制,那么是否可以表明只有当变量值2021的引用计数为0时才能触发垃圾回收机制呢?而不是上一次说的当变量值的变量名被新的变量值被引用了才会销毁呢?因为变量值可以对应多个变量名,下面通过下述实例验证:

import sys

print(sys.getrefcount(2021)) # 输出为3

year = 2021
print(sys.getrefcount(2021)) # 输出为4

height = 2021
print(sys.getrefcount(2021)) # 输出为5

year = 2022
print(sys.getrefcount(2021)) # 输出为4

del height
print(sys.getrefcount(2021)) # 输出为3

通过上述实例,可以发现由于定义一个变量后,该变量对应的变量值引用计数可以不断增加,而只要引用计数不为0,那么Python就一直还在内存中保留着这个变量值并且对其引用,只有当该变量的引用计数为0时,Python才会触发垃圾回收机制对该变量值进行回收,这才是比较正确的垃圾回收机制。当然,如果深入,Python的回收机制还有分代回收,此处不做延展,了解上述这些就足矣了解接下来讲的小整数池。

常量池

在上述各个知识的打通之后,现在可以正式引入常量池这个概念。上面讲到在Python中严格的讲是没有常量这个概念的,即使你通过约定俗成的方法定义了一个常量,但这个常量也只是一个变量,也就是说只要你对这个常量做出修改,这个常量原有对应的常量值引用计数就会变成0,由于常量等同于变量,它一样会被Python垃圾回收机制回收。

但是在Python中,存在着一些例外,这些例外就是一个小整数池,顾名思义,小整数池表示的是从-5到256范围内的整数,这些整数定义出来后就是一个常量,也就是说他们的引用计数即使为0,也不会被Python的垃圾回收机制回收,可以通过下述实例验证:

import sys

first_l = [] # 定义列表l存储[-5,256]中的所有整数的引用计数
add_l = [] # 定义列表add_l存储[-5,256]中的所有整数的引用计数加1后的引用计数
del_l = [] # 定义列表del_l存储[-5,256]中的所有整数的引用计数减1后的引用计数

for i in range(-5, 256):
  first_l.append(sys.getrefcount(i))
  add = i
  add_l.append(sys.getrefcount(i))
  del add
  del_l.append(sys.getrefcount(i))

first_l.sort()
add_l.sort()
del_l.sort()

print(f'min(first_l): {min(first_l)}') # 获取[-5,256]中所有整数的最小引用计数,输出为4
print(f'min(add_l): {min(add_l)}') # 获取[-5,256]中所有整数的最小引用计数,输出为5
print(f'min(del_l): {min(del_l)}') # 获取[-5,256]中所有整数的最小引用计数,输出为4

从上述实例可以看出,[-5,256]中的整数的getrefcount默认初始值为4,也就是说即使没有对这些整数进行初始化的创建,Python早已对他们进行了引用,即使他们的引用计数为0,他们也不会也不可能被删除,因为他们从Python解释器启动开始就已经被生成。

当然,也可以通过垃圾回收机制判断小整数池中的整数是否会被垃圾回收机制回收,可用如下实例证明(由于Pycharm等解释器会一次性编译整个文件,固使用终端编辑代码):

>>> a = 5
>>> id(a)
4529334480
>>> del a
>>> b = 5
>>> id(b)
4529334480
>>>
>>> a = 257
>>> id(a)
4533920752
>>> del a
>>> b = 257 # 消除分代回收对结果的影响
>>> del b
>>> b = 257
>>> id(b)
4531031792
>>>

从上述实例中可以看出,变量值5即使被垃圾回收机制回收后,再次创建变量值为5的变量,该变量的内存地址始终无变化,即该变量未被垃圾回收机制回收,小整数池中的其他整数同理;而变量值257却已经被垃圾回收机制回收,非小整数池中的其他变量同理。

当然,还可以通过下述方法查看这些小整数池的整数的内存地址的变化,如下:

a = 256
b = int("256")
print(id(a), id(b)) # 4544968752 4544968752

a = 257
b = int("257")
print(id(a), id(b)) # 4548719792 4546289360

a = -5
b = int("-5")
print(id(a), id(b)) # 4544960400 4544960400

a = -6
b = int("-6")
print(id(a), id(b)) # 4690036912 4546289360

对于上述实例,在Python中,由于每生成一个变量便会开辟一个新的内存空间给该变量,但是上述实例表明当变量值为-5和256时,每次开辟的内存空间地址都是一样的;而当变量值不属于[-5,256]时,每次定义变量值时,内存空间的地址都是不一样的。

总结

在Python中,变量是用来描述世间万物的,变量顾名思义是变化的一个量,而在某一个局部范围内,有些量可能是不会变化的,因此语言设计者在计算机中定义了常量这个概念,但是在Python中并没有规定的常量,只有约定俗称的常量,也就是变量名全大写的则是常量。但是Python中有一个另外,也就是小整数池[-5,256],在这个小整数池中的整数对于Python来说就是一个常量,因为从引用计数的打印中可以看出它在Python解释器启动的时候就已经生成并占用了一个固定的内存空间,并且不会因为引用计数变为0之后就会被Python的垃圾回收机制回收,而这些小整数池也可以称作Python的常量池。

以上就是详解Python垃圾回收机制和常量池的验证的详细内容,更多关于Python垃圾回收机制和常量池的验证的资料请关注我们其它相关文章!

(0)

相关推荐

  • python接口自动化如何封装获取常量的类

    这篇文章主要介绍了python接口自动化如何封装获取常量的类,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 背景: 一.执行case的过程: 首先需要,我们能够通过excel获取单元格的内容.获取内容时,首先需要知道获取的数据是哪一行的,这行数据中需要拿那些参数,比如case 名称.请求url.请求方式.header.依赖id.依赖数据所属字段.请求数据.预期结果: 然后需要,判断字段.数据的合法性,将合法case组合成接口请求: 最后需要,执

  • python常量折叠基础知识点讲解

    1.概念 所谓常量折叠,指的是在编译时就查找并计算常量表达式,而不是在运行时再对其进行计算,从而会使运行时更加精简和快速. 2.实例 在 Python 中,我们可以使用反汇编模块(Disassembler)获取 CPython 字节码,从而更好地了解代码执行的过程. 当使用dis模块反汇编上述常量表达式时,我们会得到以下字节码: >>> import dis >>> dis.dis("day_sec = 24 * 60 * 60") 0 LOAD_C

  • Python小白垃圾回收机制入门

    引用计数 Python默认的垃圾收集机制是"引用计数",每个对象维护了一个ob_ref字段.它的优点是机制简单,当新的引用指向该对象时,引用计数加1,当一个对象的引用被销毁时减1,一旦对象的引用计数为0,该对象立即被回收,所占用的内存将被释放.它的缺点是需要额外的空间维护引用计数,不过最主要的问题是它不能解决"循环引用". 什么是循环引用?A和B相互引用而再没有外部引用A与B中的任何一个,它们的引用计数虽然都为1,但显然应该被回收,例子: a = { } # a 的

  • python中的常量和变量代码详解

    局部和全局变量: # name='lhf' # def change_name(): # # global name # name='帅了一比' # print('change_name',name) # # # change_name() # print(name) # name='lhf' # # def change_name(): # name1='帅了一比' # name2='帅了一比' # name3='帅了一比' # print('change_name',name) # # ch

  • 如何快速理解python的垃圾回收机制

    一.先来说说为什么要有垃圾回收 解释器在执行到定义变量得语法时,会申请内存空间来存放变量得值,但是由于内存空间是有限得,所以这就涉及到了内存回收问题了,当一个变量值没有用了(简称垃圾),这种时候就应该回收掉这个变量值得内存空间. 二.那么什么是垃圾回收机制 垃圾回收机制(简称GC)是Python解释器自带一种机,专门用来回收不可用的变量值所占用的内存空间 三.为什么要用垃圾回收机制呢? 程序运行过程中会申请大量的内存空间,而对于一些无用的内存空间如果不及时清理的话会导致内存使用殆尽(内存溢出),

  • 深入理解Python中的内置常量

    前言 大家都知道Python内置的常量不多,只有6个,分别是True.False.None.NotImplemented.Ellipsis.__debug__.下面就来看看详细的介绍: 一. True 1. True是bool类型用来表示真值的常量. >>> True True >>> type(True) <class 'bool'> 2. 对常量True进行任何赋值操作都会抛出语法错误. >>> True = 1 SyntaxError

  • 谈谈python垃圾回收机制

    什么是垃圾回收机制? 首先,咱先来解释名词,垃圾回收是不是就是将没用的,废弃的东西回收起来. 在坐的各位都没有女朋友对吧,那难以想象你们的房间会是一个什么样子,可能会有很多垃圾,很凌乱,自己也不收拾.那当你有了女朋友了就不一样了,她会帮你收拾房间,把没用的垃圾都给你扔掉. 那在我们Python当中的垃圾回收承担的就是女朋友的角色,你们想象一下,你现在有一个轻度强迫症的女朋友一刻不停的跟在你的背后打扫卫生,你一放下脏碟子或者垃圾什么的,就已经帮你清理.打扫了 所以垃圾回收机制就是自动帮助我们管理内

  • 在Python中定义一个常量的方法

    大家都知道,网络上流行这么一句话 如果一个程序,JAVA需要写1000行,那PHP要写500行,而Python只要写200行就可以拉~~ 那么在Python中,如何用代码去实现一个常量呢? class MyNum(object): def __init__(self): self.__PI = 3.1415926 @property def PI(self): return self.__PI mynum = MyNum() print(mynum.PI) 这是通过私有属性去定义,通过装饰器@p

  • Python将字符串常量转化为变量方法总结

    前几天,我们Python猫交流学习群 里的 M 同学提了个问题.这个问题挺有意思,经初次讨论,我们认为它无解. 然而,我认为它很有价值,应该继续思考怎么解决,所以就在私密的知识星球上记录了下来. 万万没想到的是,在第二天,有两位同学接连给出了解决方法! 由此,群内出现了一轮热烈的技术交流. 本文将相关的内容要点作了梳理,并由此引申到更进一步的学习话题,希望对你有所帮助. 1.如何动态生成变量名? M 同学的问题如下: 打扰一下大家,请教一个问题,已知 list = ['A', 'B', 'C',

  • Python垃圾回收机制三种实现方法

    引用计数 Python语言默认采用的垃圾收集机制是『引用计数法 Reference Counting』,该算法最早George E. Collins在1960的时候首次提出,50年后的今天,该算法依然被很多编程语言使用. 『引用计数法』的原理是:每个对象维护一个ob_ref字段,用来记录该对象当前被引用的次数,每当新的引用指向该对象时,它的引用计数ob_ref加1,每当该对象的引用失效时计数ob_ref减1,一旦对象的引用计数为0,该对象立即被回收,对象占用的内存空间将被释放 缺点:它的缺点是需

  • 详细分析Python垃圾回收机制

    引入 为什么要有垃圾回收机制 Python中的垃圾回收机制简称(GC),我们在程序的运行中会产生大量的变量用于保存数据,而有时候有些变量已经没有用了就需要被清理释放掉该变量所占据的内存空间.在一些较为低级的语言中(比如:C语言,汇编语言)对于内存空间的释放是需要编程人员来手动进行的,这种与底层硬件直接打交道的操作是十分的危险与繁琐的,而基于C语言开发而来的Python为了解决掉这种顾虑则自带了一种垃圾回收机制,从而让开发人员不必过分担心内存的使用情况而可以全身心的投入到开发中去. >>>

  • python中的垃圾回收(GC)机制

    一.引用计数 Python 垃圾回收以引用计数为主,分代回收为辅.引用计数法的原理是每个对象维护一个ob_refcnt,用来记录对象被引用的次数,也就是用来追踪有多少个引用指向了对象,当发生以下四种情况的时候,对象的引用计数+1: 对象被创建,比如:a = 14 对象被引用,比如: b = a 对象被作为参数,传给函数,比如:func(a) 对象作为容器中的一个元素,比如:List = {a, "a" , "b", 2} 与上述情况相对应,当发生以下四种情况时,对象

随机推荐