首页 科普 资讯 养生 问答 找医院 相关问答
首页> 问答

从零开始学 Python 之 Python 之引用

发布网友 发布时间:2024-09-24 02:01

我来回答

1个回答

热心网友 时间:2024-09-28 01:32

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 ring its lifetime. Two objects with non-overlapping lifetimes may have the same id() value.

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

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

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

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

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

1.2 运算符is运算含义isobject ?identity

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

——引自《Python 3.7.4 文档-内置类型》

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().

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

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

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

2. 问题示例

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

2.1 对同一变量赋值

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

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

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

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

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

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

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

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

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

得到的id值也是不同的。

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

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

>>> littleConst = 1 # 数值较小的整型对象>>> id(littleConst)140734357607232>>> littleConst = 1>>> id(littleConst)140734357607232>>> id(1)140734357607232

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

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

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

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

>>> change_ref = 10000>>> id(change_ref)2161457772304>>> change_ref = change_ref + 1>>> change_ref10001>>> id(change_ref)2161457772880

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

>>> list_change_ref = [1,2]>>> id(list_change_ref)2161458326920>>> list_change_ref = list_change_ref + [4]>>> list_change_ref[1, 2, 4]>>> id(list_change_ref)21614583427922.2 不变的情况

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

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

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

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

>>> list_example = [1, 2, 3]>>> list_same_ref = list_example>>> id(list_example)1823864610120>>> id(list_same_ref)1823864610120

显然此时的变量list_example和list_same_ref的id是一致的。现在改变list_example所引用的列表值:

>>> python_variable = 10000>>> id(python_variable)1823863879824>>> python_variable = 12345>>> id(python_variable)18238638801760

可以看到list_same_ref所引用的列表值也随之变化了。再看看相应地id:

>>> python_variable = 10000>>> id(python_variable)1823863879824>>> python_variable = 12345>>> id(python_variable)18238638801761

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

>>> list_example.append(3)>>> list_example[1, 2, 5, 3]>>> list_same_ref[1, 2, 5, 3]>>> python_variable = 10000>>> id(python_variable)1823863879824>>> python_variable = 12345>>> id(python_variable)18238638801761

删除元素:

>>> del list_example[3]>>> list_example[1, 2, 5]>>> list_same_ref[1, 2, 5]>>> python_variable = 10000>>> id(python_variable)1823863879824>>> python_variable = 12345>>> id(python_variable)18238638801761

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

也就是说,对于变量本身进行的操作并不会创建新的对象,而是会直接改变原有对象的值。

2.3 一个特殊的地方

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

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

>>> python_variable = 10000>>> id(python_variable)1823863879824>>> python_variable = 12345>>> id(python_variable)18238638801764

有了前面的铺垫,这样的结果很显得很自然。显然在对变量num进行增1操作的时候,还是计算出新值然后进行赋值操作,因此引用发生了变化。

但列表却不然。见如下代码:

>>> python_variable = 10000>>> id(python_variable)1823863879824>>> python_variable = 12345>>> id(python_variable)18238638801765

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

实际上这是因为加法运算符在 Python 中存在重载的情况,对列表对象和数值对象来说,加法运算的底层实现是完全不同的,在简单的加法中,列表的运算还是创建了一个新的列表对象;但在简写的加法运算+=实现中,则并没有创建新的列表对象。这一点要十分注意。

3. 原理解析

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

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

>>> python_variable = 10000>>> id(python_variable)1823863879824>>> python_variable = 12345>>> id(python_variable)18238638801766

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

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

而作为可变对象,列表的值是可以在不新建对象的情况下进行改变的,因此对列表对象本身直接进行操作,是可以达到“改变变量值而不改变引用”的目的的。

4. 总结

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

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

示例代码:Python-100-Days

原文:https://juejin.cn/post/7103441305623068703
苹果电脑电池充不进电苹果电脑充不进去电是怎么回事 苹果电脑不充电没反应苹果电脑充电指示灯不亮充不了电怎么办 狗狗更加忠诚护家、善解人意,养一只宠物陪伴自己,泰迪能长多大... 描写泰迪狗的外形和特点的句子 国外留学有用吗 花钱出国留学有用吗 !这叫什么号 百万医疗赔付后是否可以续保 前一年理赔过医疗险还能续保吗? 医疗住院险理赔后还能购买吗? 女生多大后可以不在长身高? 如何不用软件把手机投屏到电脑上手机屏幕怎样投放到电脑上 战时拒绝、故意延误军事订货罪既遂的处罚? 战时故意延误军事订货罪处罚标准 名师1+1导读方案:汤姆·索亚历险记目录 三星sm-g7200打开微信慢,无法正常收看,网速不慢。 笔记本电脑如何调亮屏幕亮度 大伙说说洗衣机要不要带烘干好 热烘干洗衣机怎么样 ef英语哪个好 EF英孚英语培训怎么样? 英孚英语好不好 EF英孚教育到底好不好 大佬们,麦芒7和荣耀10那个值得入手?2500以下的机子还有啥好推荐的么... 介绍几款2500元以前的手机 像素一定要高 其他的不做要求 近期想入手一部安卓手机,价格2200到2500左右…买HTC desire Z还是 三星... 笔记本忘记开机密码怎么办急死了 笔记本电脑屏幕开机锁忘记密码 怎么办?急死了 华硕笔记本电脑开机密码忘记了怎样找回?系统是Windows 7旗舰版... 我的HP笔记本的一开始进入程序的用户名忘记了,怎么办啊~ 笔记本电脑开机密码不记得了.要怎么办,才能登陆阿.急死了 笔记本电脑忘记开机密码了怎么办?急 如何判断狗狗是不是有犬瘟热呢? 姓黄的男孩有哪些好名 黄姓男孩名字大全2025年属蛇 男宝宝阳光帅气名集锦 右卵巢内囊性结构是什么病 减法和差的概念? a减b与c的积,差是几? “不须苦道丰年瑞”的出处是哪里 “从前浪说丰年瑞”的出处是哪里 “昌黎浪说丰年瑞”的出处是哪里 “数载太平丰年瑞”的出处是哪里 “天公已作丰年瑞”的出处是哪里 “疲民尽说丰年瑞”的出处是哪里 “朝来已贺丰年瑞”的出处是哪里 厕所蹲坑怎么安装 蹲坑如何安装 如何安装蹲坑 vivo手机x21屏幕怎么拆vivox21拆屏幕 魔兽考古诀窍 动手理解Python的引用、赋值、拷贝 redmi蓝牙耳机双耳模式怎么切换使用? 华为8寸平板进电话视频频目总是倒过来 平板电脑不知哪里弄错了,怎么换屏过来,那位大神 ...讲的是一个长得像猴子一样的鬼在一个房子里只有晚上出来能打地洞... 工可报告和可研报告是一回事吗 能评报告需要依据可研报告吗 成都地铁2号线一期工程可研报告通过专家评审 怎么样能把vivo账号的实名改成自己的 vivo账号实名认证不是本人怎么改 南沙湿地好玩不? excel如何在单元格中输入日期和星期呢? 如何自动在EXCEL中输入时间? 男人真的爱一个女人,私下会吻她吗? 吻过女人这2个地方的男人,才是真爱吗? 能亲你下面人是真心喜欢你的人吗 上古卷轴5种族选哪个好-种族选择推荐 男人亲女人私下是不是真爱??? 男人亲吻女人的哪里能证明是真爱呢 玩游戏小惩罚有哪些 Python中函数参数传递方法*args, **kwargs,还有其他 深圳二手房屋交易合同范本2022 为什么采用开口过渡环减小薄壁的变形 柿树春接好成活 自家房子用eps好还是grc 如何使用小米手机助手升级小米手机系统? ...的烤瓷牙2000元的和4000元的有什么区别呢?医生说是德国进口的... 如何才能申请上微信个人公众号 个人如何注册微信公众号 物业费包含垃圾处理费吗街道又收垃圾费 垃圾费是不是包含物业费用里面 VIVO手机微信怎么切换成扬声器? 全国较大的二手车市场在哪里 批量复制粘贴内容的方法有哪些? 粘贴文字的快捷键是什么 不带格式粘贴快捷键_不带格式粘贴快捷键word 怎么把一个表格复制粘贴多次 红罂粟2分集剧情 充电器不充电时插在插座上会耗电吗 充电器只要插在电源上就会耗电吗
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com