第17天:Python 之引用


                                                                 

图片

1. 引用简介与工具引入

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

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

1.1 内置函数id()

id(object)编程

Return  the “identity” of an object. This is an integer which is guaranteed to  be unique and constant for this object during its lifetime. Two objects  with non-overlapping lifetimes may have the same `id()`[1] value.数据结构

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

CPython implementation detail: This is the address of the object in memory.ide

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

——引自《Python 3.7.4 文档-内置函数-id()[2]工具

换句话说,不管是否是 CPython 实现,一个对象的id就能够视做是其虚拟的内存地址。this

1.2 运算符is

运算 含义
is object  identity

is的做用是比较对象的标识。spa

——引自《Python 3.7.4 文档-内置类型[3]

1.3 sys模块函数getrefcount()函数

sys.getrefcount(object)

Return the reference count of the object.  The count returned is generally one higher than you might expect,  because it includes the (temporary) reference as an argument to `getrefcount()`[4].

返回值是传入对象的引用计数。因为做为参数传入getrefcount()的时候产生了一次临时引用,所以返回的计数值通常要比预期多1。

——引自《Python 3.7.4 文档-sys模块——系统相关参数及函数[5]

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

2. 问题示例

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

2.1 对同一变量赋值

举例来讲,对于以下的 C 代码:

int a = 10000;printf("original address: %p\n", &a); // original address: 0060FEFCa = 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>>> a10001>>> id(a)2161457772880

比较代码块第 三、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

2.2 不变的状况

与数值不一样,Python 中对列表对象的操做还表现出另外一种特性。考虑下面的代码:

>>> c = [1, 2, 3]>>> id(c)2161458355400>>> c[2] = 5>>> c[1, 2, 5]>>> id(c)2161458355400>>> c.append(3)>>> c[1, 2, 5, 3]>>> id(c)2161458355400

观察代码块第 三、八、13三行,输出相同。也就是说,对于列表而言,能够经过直接操做变量自己,从而在不改变其引用的状况下改变所引用的值。

更进一步地,若是是两个变量同时引用同一个列表,则对其中一个变量自己直接进行操做,也会影响到另外一个变量的值:

>>> c = [1, 2, 3]>>> cc = c>>> id(c)1823864610120>>> id(cc)1823864610120

显然此时的变量cccid是一致的。如今改变c所引用的列表值:

>>> c[2] = 5>>> cc[1, 2, 5]

能够看到cc所引用的列表值也随之变化了。再看看相应地id

>>> id(c)1823864610120>>> id(cc)1823864610120

两个变量的id都没有发生变化。再调用append()方法:

>>> c.append(3)>>> c[1, 2, 5, 3]>>> cc[1, 2, 5, 3]>>> id(c)1823864610120>>> id(cc)1823864610120

删除元素:

>>> del c[3]>>> c[1, 2, 5]>>> cc[1, 2, 5]>>> id(c)1823864610120>>> id(cc)1823864610120

在上述全部对列表的操做中,均没有改变相应元素的引用。

也就是说,对于变量自己进行的操做并不会建立新的对象,而是会直接改变原有对象的值。

2.3 一个特殊的地方

本小节示例灵感来自[关于Python中的引用[7]]

数值数据和列表还存在一个特殊的差别。考虑以下代码:

>>> num = 10000>>> id(num)2161457772336>>> num += 1>>> id(num)2161457774512

有了前面的铺垫,这样的结果很显得很天然。显然在对变量num进行增1操做的时候,仍是计算出新值而后进行赋值操做,所以引用发生了变化。

但列表却否则。见以下代码:

>>> li = [1, 2, 3]>>> id(li)2161458469960>>> li += [4]>>> id(li)2161458469960>>> li[1, 2, 3, 4]

注意第 4 行。明明进行的是“相加再赋值”操做,为何有了跟前面不同的结果呢?检查变量li的值,发现变量的值也确实发生了改变,但引用却没有变。

实际上这是由于加法运算符在 Python 中存在重载的状况,对列表对象和数值对象来讲,加法运算的底层实现是彻底不一样的,在简单的加法中,列表的运算仍是建立了一个新的列表对象;但在简写的加法运算+=实现中,则并无建立新的列表对象。这一点要十分注意。

3. 原理解析

前面(第3天:Python 变量与数据类型[8])咱们提到过,Python 中的六个标准数据类型实际上分为两大类:可变数据不可变数据。其中,列表、字典和集合均为“可变对象”;而数字、字符串和元组均为“不可变对象”。实际上上面演示的数值数据(即数字)和列表之间的差别正是这两种不一样的数据类型致使的。

因为数字是不可变对象,咱们不可以对数值自己进行任何能够改变数据值的操做。所以在 Python 中,每出现一个数值都意味着须要另外分配一个新的内存空间(常量池中的数值例外)。

>>> a = 10000>>> a == 10000True>>> a is 10000False>>> id(a)2161457773424>>> id(10000)2161457773136>>> from sys import getrefcount>>> getrefcount(a)2>>> getrefcount(10000)3

前 9 行的代码容易理解:即便是一样的数值,也可能具备不一样的引用值。关键在于这个值是否来自于同一个对象。

而第 10 行的代码则说明除了getrefcount()函数的引用外,变量a所引用的对象就只有1个引用,也就是变量a。一旦变量a被释放,则相应的对象引用计数归零,也会被释放;而且只有此时,这个对象对应的内存空间才是真正的“被释放”。

而做为可变对象,列表的值是能够在不新建对象的状况下进行改变的,所以对列表对象自己直接进行操做,是能够达到“改变变量值而不改变引用”的目的的。

4. 总结

对于列表、字典和集合这些“可变对象”,经过对变量所引用对象自己进行操做,能够只改变变量的值而不改变变量的引用;但对于数字、字符串和元组这些“不可变对象”,因为对象自己是不可以进行变值操做的,所以要想改变相应变量的值,就必需要新建对象,再把新建对象赋值给变量。

经过这样的探究,也能更加生动地理解“万物皆对象”的深入含义。

5. 参考资料

Python 3.7.4 文档-内置函数-id()[9]

Python 3.7.4 文档-内置类型[10]

Python 3.7.4 文档-sys模块——系统相关参数及函数[11]

Python 3.7.4 文档-术语表[12]

关于Python中的引用[13]

[1]

id(): https://docs.python.org/3.7/library/functions.html?highlight=id#id

[2]

Python 3.7.4 文档-内置函数-id(): https://docs.python.org/3.7/library/functions.html?highlight=id#id

[3]

Python 3.7.4 文档-内置类型: https://docs.python.org/3/library/stdtypes.html

[4]

getrefcount(): https://docs.python.org/3.7/library/sys.html#sys.getrefcount

[5]

Python 3.7.4 文档-sys模块——系统相关参数及函数: https://docs.python.org/3.7/library/sys.html#sys.getrefcount

[6]

Python 文档: https://docs.python.org/3.7/glossary.html?highlight=getrefcount

[7]

[关于Python中的引用: https://www.cnblogs.com/yuyan/archive/2012/04/21/2461673.html

[8]

第3天:Python 变量与数据类型: http://www.ityouknow.com/python/2019/08/03/python-003.html

[9]

Python 3.7.4 文档-内置函数-id(): https://docs.python.org/3.7/library/functions.html?highlight=id#id

[10]

Python 3.7.4 文档-内置类型: https://docs.python.org/3/library/stdtypes.html

[11]

Python 3.7.4 文档-sys模块——系统相关参数及函数: https://docs.python.org/3.7/library/sys.html#sys.getrefcount

[12]

Python 3.7.4 文档-术语表: https://docs.python.org/3.7/glossary.html?highlight=getrefcount

[13]

关于Python中的引用: https://www.cnblogs.com/yuyan/archive/2012/04/21/2461673.html

系列文章   第11天:Python 字典

第10天:Python 类与对象

第9天:Python Tupple

第8天:Python List

第7天:Python 数据结构--序列

第6天:Python 模块和

第5天:Python 函数

第4天:Python 流程控制

第3天:Python 变量与数据类型

第2天:Python 基础语法

第1天:Python 环境搭建

相关文章
相关标签/搜索