Python 之引用

1. 引用简介与工具引入

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

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

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()返回值。

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

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

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

1.2 运算符is

运算含义isobject identity即is的作用是比较对象的标识。

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()的时候产生了一次临时引用,因此返回的计数值一般要比预期多1。

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

此处的“引用计数”,在 Python 文档中被定义为“对象被引用的次数”。一旦引用计数归零,则对象所在的内存被释放。这是 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一直都是相同的,没有发生变化,因此变量a的id也就没有变化。

这是因为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

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

观察代码块第 3、8、13三行,输出相同。也就是说,对于列表而言,可以通过直接操作变量本身,从而在不改变其引用的情况下改变所引用的值。

更进一步地,如果是两个变量同时引用同一个列表,则对其中一个变量本身直接进行操作,也会影响到另一个变量的值:

>>> c = [1, 2, 3]

>>> cc = c

>>> id(c)

1823864610120

>>> id(cc)

1823864610120

显然此时的变量c和cc的id是一致的。现在改变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 一个特殊的地方

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

>>> 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 变量与数据类型)我们提到过,Python 中的六个标准数据类型实际上分为两大类:可变数据和不可变数据。其中,列表、字典和集合均为“可变对象”;而数字、字符串和元组均为“不可变对象”。实际上上面演示的数值数据(即数字)和列表之间的差异正是这两种不同的数据类型导致的。

由于数字是不可变对象,我们不能够对数值本身进行任何可以改变数据值的操作。因此在 Python 中,每出现一个数值都意味着需要另外分配一个新的内存空间(常量池中的数值例外)。

>>> a = 10000

>>> a == 10000

True

>>> a is 10000

False

>>> 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 id函数 引用本身地址_Python 之引用相关推荐

  1. Python id() 函数

    Python id() 函数  Python 内置函数 描述 id() 函数用于获取对象的内存地址. 语法 id 语法: id([object]) 参数说明: object -- 对象. 返回值 返回 ...

  2. python函数传值还是地址_python中给函数传参是传值还是传引用

    首先还是应该科普下函数参数传递机制,传值和传引用是什么意思? 函数参数传递机制问题在本质上是调用函数(过程)和被调用函数(过程)在调用发生时进行通信的方法问题.基本的参数传递机制有两种:值传递和引用传 ...

  3. python回调函数实例详解_Python回调函数用法实例详解

    本文实例讲述了Python回调函数用法.分享给大家供大家参考.具体分析如下: 一.百度百科上对回调函数的解释: 回调函数就是一个通过函数指针调用的函数.如果你把函数的指针(地址)作为参数传递给另一个函 ...

  4. python关于函数作用的描述_Python函数的概念和使用

    函数 为了便于程序的维护和更好的实现模块化,好的程序都会分解为很多函数. 可以这么说,对于任何的编程语言,函数都是一个非常重要的概念. python 不仅简化了函数的定义过程,而且还大量借鉴了其他函数 ...

  5. python定义函数的组成部分有_Python文档学习笔记(4)--定义函数

    定义函数 关键字 def 引入函数的定义.其后必须跟有函数名和以括号标明的形式参数列表.组成函数体的语句从下一行开始,且必须缩进. 执行 一个函数会引入一个用于函数的局部变量的新符号表. 因此,在函数 ...

  6. python定义函数的关键字是_python 函数定义

    python 函数 ---------------- ---------------- ---------------- ---------------- def test(x): print(x) ...

  7. python的函数的对象属性_Python帮助函数调试函数 用于获取对象的属性及属性值...

    Python帮助函数调试函数 用于获取对象的属性及属性值 刚接触Python,上篇 <Python入门>第一个Python Web程序--简单的Web服务器 中调试非常不方便,不知道对象详 ...

  8. python中函数的定义实例_Python基础之函数的定义与使用实例

    此文实例介绍了Python基础之函数的定义与使用.推荐给大伙学习一下,内容如下: Python 定义函数使用 def 关键字,一般格式如下: def 函数名(参数列表): 函数体 让我们使用函数来输出 ...

  9. python open函数 创建变量文件_python的open函数怎么用

    python open() 函数用于打开一个文件,创建一个 file 对象,相关的方法才可以调用它进行读写. 函数语法open(name[, mode[, buffering]]) 参数说明: nam ...

  10. python range函数怎么表示无限_python range函数怎么用

    python range函数怎么用? python range() 函数可创建一个整数列表,一般用在 for 循环中. 函数语法range(start, stop[, step]) 参数说明: sta ...

最新文章

  1. PingingLab传世经典系列《CCNA完全配置宝典》-5.8 静态NAT
  2. java 混码_kotlin java 混合代码 maven 打包实现
  3. bay trail android 平板,英特尔再推9款平板专用BayTrail-T处理器
  4. json标准格式举例_JSON格式简介及一些对应函数
  5. linux进程泄漏如何定位,定位Linux下定位进程被谁KILL
  6. StanfordDB class自学笔记 (8) Querying XML
  7. 中国天气网城市编码获取地址
  8. Android源码下载repo以及repo init总结
  9. 简支梁挠度计算公式推导_自己整理的简支梁挠度计算公式
  10. 使用快捷指令和carplay发送停车位置(高德地图)
  11. 网页视频加速播放方法(不用下载插件,不用安装浏览器)
  12. 携程四君子:中国最美创业故事
  13. 使用思科模拟器 Cisco Packet Tracer 模拟交换机基本配置
  14. aspectjweaver.jar+ aopalliance.jar+mchange-commons-java.jar+cglib.jar官网下载
  15. java中this关键变量
  16. 再谈谷歌搜索引擎使用技巧
  17. 安装 - LNMP一键安装包
  18. 为My97DatePicker日期插件设置默认日期
  19. can总线分析仪与stm32的收发操作
  20. 谷歌浏览器(电脑端)

热门文章

  1. JAVA语法基础作业——动手动脑以及课后实验性问题 (七)
  2. Windows server 2003 伪静态配置方法
  3. POJ 1947 Rebuilding Roads
  4. ios音乐播放器使用综述
  5. MEF的asp.net Hello World程序
  6. Atitit 遍历 与循环模式大总结 目录 1.1. 遍历的对象 数组 或对象 或对象数组 1 2. 遍历的概念 2 2.1. 祖先后代同胞 过滤 2 3. 常见的遍历四种方式 2 3.1.
  7. Atitit.并发编程原理与概论 attilax总结
  8. atitit.项目设计模式---ioc attilax总结
  9. paip.提升用户体验---c++ 右键菜单以及socket接口
  10. paip.C#.net 悬浮窗口的建立总结