Python变量、垃圾回收与小整数池机制深度解析

七月
七月
发布时间:2026-10-14 | 6人浏览过
原创

本文深入解析Python的变量存储、垃圾回收机制及小整数池原理。通过引用计数、id和getrefcount函数,详细演示内存地址变化与对象生命周期。重点验证-5至256范围内整数作为常量池对象的特殊行为,解释其为何不被回收及内存复用机制。

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年,不会有人称其为2022年。具备丰富开发经验者明白,变量定义并非仅用于展示,更多是为了使用。若在2021年的程序中需要使用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垃圾回收机制和常量池的验证的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表