通过代码验证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垃圾回收机制和常量池的验证的更多相关文章

  1. python垃圾回收机制与小整数池

    python垃圾回收机制 当引用计数为0时,python会删除这个值. 引用计数 x = 10 y = x del x print(y) 10 引用计数+1,引用计数+1,引用计数-1,此时引用计数为 ...

  2. 从 CPython 源码角度看 Python 垃圾回收机制

    环状双向链表 refchain 在 Python 程序中创建的任何对象都会被放到 refchain 链表中,当创建一个 Python 对象时,内部实际上创建了一些基本的数据: 上一个对象 下一个对象 ...

  3. python垃圾回收机制:引用计数 VS js垃圾回收机制:标记清除

    js垃圾回收机制:标记清除 Js具有自动垃圾回收机制.垃圾收集器会按照固定的时间间隔周期性的执行. JS中最常见的垃圾回收方式是标记清除. 工作原理 当变量进入环境时,将这个变量标记为"进入 ...

  4. 补充:垃圾回收机制、线程池和ORM缺点

    补充:垃圾回收机制.线程池和ORM缺点 垃圾回收机制不仅有引用计数,还有标记清除和分代回收 引用计数就是内存地址的门牌号,为0时就会回收掉,但是会出现循环引用问题,这种情况下会导致内存泄漏(即不会被用 ...

  5. 浅析Python垃圾回收机制!

    Python垃圾回收机制 目录 Python垃圾回收机制 1. 内存泄露 2. Python什么时候启动垃圾回收机制? 2.1 计数引用 2.2 循环引用 问题:引用计数是0是启动垃圾回收的充要条件吗 ...

  6. Python垃圾回收机制 总结

    Python 垃圾回收机制 内存管理 Python中的内存管理机制的层次结构提供了4层,其中最底层则是C运行的malloc和free接口,往上的三层才是由Python实现并且维护的,第一层则是在第0层 ...

  7. python垃圾回收机制的一些理解

    概览:       主要通过 引用计数来进行垃圾收集, 就是说,当一个对象没有被其他对象引用的时候,会释放掉内存.     但是会有一些循环引用的对象,通过上面的方法,是没有办法清除掉的.所以,pyt ...

  8. Python垃圾回收机制详解

    一.垃圾回收机制 Python中的垃圾回收是以引用计数为主,分代收集为辅.引用计数的缺陷是循环引用的问题. 在Python中,如果一个对象的引用数为0,Python虚拟机就会回收这个对象的内存. #e ...

  9. python 垃圾回收机制的思考

    一.前言 Python 是一门高级语言,使用起来类似于自然语言,开发的时候自然十分方便快捷,原因是Python在背后为我们默默做了很多事情,其中一件就是垃圾回收,来解决内存管理,内存泄漏的问题. 内存 ...

随机推荐

  1. 史上最全Redis面试题(含答案):哨兵+复制+事务+集群+持久化等

    Redis主要有哪些功能? 哨兵(Sentinel)和复制(Replication) Redis服务器毫无征兆的罢工是个麻烦事,如何保证备份的机器是原始服务器的完整备份呢?这时候就需要哨兵和复制. S ...

  2. 踏上Revit二次开发之路 3 自己的工具按钮

    3 自己的工具按钮 上次的例子只能在"附加模块"→"外部工具"下运行,用作个人作品是没问题,如果打算搞个公司产品的话,估计BOSS是不会满意的.这次我来做一个直 ...

  3. 梨子带你刷burp练兵场(burp Academy) - 服务器篇 - Sql注入 - SQL injection UNION attack, determining the number of columns returned by the query

    目录 SQL injection UNION attack, determining the number of columns returned by the query SQL injection ...

  4. Mac下anaconda的安装和基本使用

    Mac下anaconda的安装和基本使用 安装 在conda官网下载安装conda. 打开terminal输入conda -V,回车显示conda的版本说明安装成功. 将conda更新到最新版本 co ...

  5. k8s二进制部署 - 总结

    镜像仓库: 安装软件:docker.docker-compose.harbor.nginx 1.下载cfssl.cfssljson.cfssl-certinfo,增加执行权限并放在PATH环境变量路径 ...

  6. ZOJ 3430 Detect the Virus(AC自动机 + 模拟)题解

    题意:问你主串有几种模式串.但是所有串都是加密的,先解码.解码过程为:先把串按照他给的映射表变成6位数二进制数,然后首尾衔接变成二进制长串,再8位8位取变成新的数,不够的补0.因为最多可能到255,所 ...

  7. 链接脚本再探和VMA与LMA

    链接脚本简单描述 连接脚本的描述都是以节(section)的单位的,网上也有很多描述链接脚本语法的好文章,再不济还有官方的说明文档可以用来学习,其实主要就是对编译构建的整个过程有了深入的理解后就能对链 ...

  8. 如何禁用 Chrome Taps Group feature 💩

    如何禁用 Chrome Taps Group feature bug https://support.google.com/chrome/go/feedback_confirmation How to ...

  9. Baccarat凭什么能成为DeFi后时代火爆新趋势?

    在各币种经历涨涨跌跌以后,DeFi后时代已然来临.那么,当前DeFi市场中哪个项目更被市场生态建设者看好呢?毫无疑问,Baccarat会成为最被看好的DeFi项目. Baccarat采用了独特的共识算 ...

  10. [转]在ROS下使用zeroconf配置多机通信

    原文地址:http://www.corvin.cn/635.html,转载主要方便随时查阅,如有版权要求,请及时联系. 0x00 为何需要配置ROS多机通信 众所周知ROS是分布式系统,因此可以将机器 ...