Python学习第17天:Python 引用[1]

89 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情 >>

1. 引用简介与工具引入

Python 中对于变量的处理与 C 语言有着很大的不同,Python 中的变量具有一个特殊的属性:identity,即“身份标识”。这种特殊的属性也在很多地方被称为“引用”。

为了更加清晰地说明引用相关的问题,我们首先要介绍两个工具:一个Python的内置函数:id();一个运算符:is;同时还要介绍一个sys模块内的函数:getrefcount()

1.1 内置函数id()

id(object)

返回值为传入对象的“标识”。该标识是一个唯一的常数,在传入对象的生命周期内与之一一对应。生命周期没有重合的两个对象可能拥有相同的id()返回值。

CPython 实现细节:“标识”实际上就是对象在内存中的地址。

换句话说,不论是否是 CPython 实现,一个对象的id就可以视作是其虚拟的内存地址。

1.2 运算符is

运算含义
isobject  identity

is的作用是比较对象的标识。

1.3 sys模块函数getrefcount()函数

sys.getrefcount(object)

返回值是传入对象的引用计数。由于作为参数传入getrefcount()的时候产生了一次临时引用,因此返回的计数值一般要比预期多1。

此处的“引用计数”,在 Python 文档[6]中被定义为“对象被引用的次数”。一旦引用计数归零,则对象所在的内存被释放。这是 Python 内部进行自动内存管理的一个机制。

2. 问题示例

C 语言中,变量代表的就是一段固定的内存,而赋给变量的值则是存在这段地址中的数据;但对 Python 来说,变量就不再是一段固定的地址,而只是 Python 中各个对象所附着的标签。理解这一点对于理解 Python 的很多特性十分重要。

2.1 对同一变量赋值

举例来说,对于如下的 C 代码:

int a = 10000;
printf("original address: %p\n", &a); // original address: 0060FEFC
a = 12345;
printf("second address: %p\n", &a); // second address: 0060FEFC

对于有 C 语言编程经验的人来说,上述结果是显而易见的:变量a的地址并不会因为赋给它的值有变化而发生变化。对于 C 编译器来说,变量a只是协助它区别各个内存地址的标识,是直接与特定的内存地址绑定的,如图所示:

图片但 Python 就不一样的。考虑如下代码:

>>> a = 10000
>>> id(a)
1823863879824
>>> a = 12345
>>> id(a)
1823863880176

这就有点儿意思了,更加神奇的是,即使赋给变量同一个常数,其得到的id也可能不同:

>>> a = 10000
>>> id(a)
1823863880304
>>> a = 10000
>>> id(a)
1823863879408

假如a对应的数据类型是一个列表,那么:

>>> a = [1,2]
>>> id(a)
2161457994952
>>> a = [1,2]
>>> id(a)
2161458037448

得到的id值也是不同的。

正如前文所述,在 Python 中,变量就是一块砖,哪里需要哪里搬。每次将一个新的对象赋值给一个变量,都在内存中重新创建了一个对象,这个对象就具有新的引用值。作为一个“标签”,变量也是哪里需要哪里贴,毫无节操可言。

图片

但要注意的是,这里还有一个问题:之所以说“即使赋给变量同一个常数,其得到的id可能不同”,实际上是因为并不是对所有的常数都存在这种情况。以常数1为例,就有如下结果:

>>> a = 1
>>> id(a)
140734357607232
>>> a = 1
>>> id(a)
140734357607232
>>> id(1)
140734357607232

可以看到,常数1对应的id一直都是相同的,没有发生变化,因此变量aid也就没有变化。

这是因为Python在内存中维护了一个特定数量的常量池,对于一定范围内的数值均不再创建新的对象,而直接在这个常量池中进行分配。实际上在我的机器上使用如下代码可以得到这个常量池的范围是 [0, 256] ,而 256 刚好是一个字节的二进制码可以表示的值的个数。

for b in range(300):
    if b is not range(300)[b]:
        print("常量池最大值为:", (b - 1))
        break
# 常量池最大值为:256

相应地,对于数值进行加减乘除并将结果赋给原来的变量,都会改变变量对应的引用值:

>>> a = 10000
>>> id(a)
2161457772304
>>> a = a + 1
>>> a
10001
>>> id(a)
2161457772880

比较代码块第 3、8行的输出结果,可以看到对数值型变量执行加法并赋值会改变对应变量的引用值。这样的表现应该比较好理解。因为按照 Python 运算符的优先级,a = a + 1实际上就是a = (a + 1),对变量a对应的数值加1之后得到的是一个新的数值,再将这个新的数值赋给a ,于是a的引用也就随之改变。列表也一样:

>>> a = [1,2]
>>> id(a)
2161458326920
>>> a = a + [4]
>>> a
[1, 2, 4]
>>> id(a)
2161458342792