第17天: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()`[1] value.

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

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

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

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

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

1.2 运算符is

运算 含义
is object  identity

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

——引自《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

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

显然此时的变量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

系列文章
(0)

相关推荐

  • python基础入门教程:传参是传值还是传引用

    在此之前先来看看变量和对象的关系:Python 中一切皆为对象,数字是对象,列表是对象,函数也是对象,任何东西都是对象.而变量是对象的一个引用(又称为名字或者标签),对象的操作都是通过引用来完成的.例 ...

  • 零基础小白Python入门必看——编程基础概念

    . 程序的构成 程序由模块组成,一个模块对应python的源文件 ,一般后缀为:.py 模块由语句构成 语句是python程序的构造单元,用于创建对象.变量赋值.调用函数.控制语句等. 2. 对象 每 ...

  • Python面试的50个经典问答(上)

    Python面试的50个经典问答(上)

  • 说说Python中变量的作用域?

    公众号新增加了一个栏目,就是每天给大家解答一道Python常见的面试题,反正每天不贪多,一天一题,正好合适,只希望这个面试栏目,给那些正在准备面试的同学,提供一点点帮助! 小猿会从最基础的面试题开始, ...

  • 面试题-python 垃圾回收机制?

    前言 简历上写着熟悉 python 面试官上来就问:说下python 垃圾回收机制?一盆冷水泼过来,瞬间感觉 python 不香了. Python中,主要通过引用计数(Reference Counti ...

  • python - 内置对象 之 变量

    一.变量命名规划 1.命名规则 (1)命名内容只能是字母.下划线.数字 (2)名字第1字符只能是字母或下划线 (3)区分大小写 2.私有变量 (1)_xxx "单下划线" 开始的成 ...

  • 可变对象与不可变对象

    前阵子我们聊了下函数的参数传递以及变量赋值的一些内容:关于函数参数传递,80%人都错了. 简单回顾下要点: 1. Python 中的变量不是装有对象的"容器",而是贴在对象上的&q ...

  • 再谈Python的引用和变量

    来源:Python 技术「ID: pythonall」 再谈Python的引用和变量 上一次我们介绍了一个有用的代码可视化工具Python Tutor,说到我们还要通过这个工具再探讨一下Python引 ...

  • 面试分享:17道Python面试题,让你在求职中无往不利

    今天给大家分享的是Python面试题系列的第一篇文章,后续我也会陆续整理Python相关的问题给大家,无论是求职者还是新人都可以通过面试题来考察自己的能力缺陷. 1.Python中pass语句的作用是 ...

  • 你可能不知道的17个Python 技巧

    译者 | 豌豆花下猫 有许许多多文章写了 Python 中的许多很酷的特性,例如变量解包.偏函数.枚举可迭代对象,但是关于 Python 还有很多要讨论的话题,因此在本文中,我将尝试展示一些我知道的和 ...

  • 天才美少女!她13岁自己造飞机,17岁进麻省理工,3篇黑洞论文被霍金引用......

    13岁的时候,你在干嘛? 我想,绝大多数人都没有萨布丽娜这么硬核--独自组装飞机. 之后,16岁的萨布丽娜完成了生涯首飞,历史第一年轻. 17岁时,她考上麻省理工,毕业成绩全班第一. 如今,28岁的萨 ...

  • Python基础手册17——简单语句

    Python语句 Python的语法实际上是由语句和表达式组成的(语句使用关键字来组成,只执行某种操作,可以没有返回值:而表达式没有关键字,是使用数学运算符构成的算数表达式,是一个单纯的运算求值过程, ...

  • python笔记49-yaml文件中变量的使用(锚点& 与 引用*)

    前言 在yaml文件中如何引用变量?当我们在一个yaml文件中写很多测试数据时候,比如一些配置信息像用户名,邮箱,数据库配置等很多地方都会重复用到. 重复的数据,如果不设置变量,后续维护起来就很困难. ...

  • 第115天:Python 到底是值传递还是引用传递

    我们平时写的 Python 程序中充斥着大量的函数,包括系统自带函数和自定义函数,当我们调用函数时直接将参数传递进去然后坐等接收返回值即可,简直不要太好用.那么你知道函数的参数是怎么传递的么,是值传递 ...

  • Python模块之间的相互引用问题

    在某次运行过程中出现了如下两个报错: 报错1: ModuleNotFoundError: No module named '__main__.src_test1'; '__main__' is not ...