如何理解 Python 的赋值逻辑

news2024/12/26 21:23:54

摘要:
如果你学过 C 语言,那么当你初见 Python 时可能会觉得 Python 的赋值方式略有诡异:好像差不多,但又好像哪里有点不太对劲。

本文比较并解释了这种赋值逻辑上的差异。回答了为什么需要这种赋值逻辑以及如何使用这种赋值逻辑的问题。

当然,即使未学过 C 语言,也可通过本文更好地了解 Python 的赋值逻辑——这种赋值逻辑影响着 Python 的方方面面,从而可以让你更好地理解和编写 Python 程序。

第一章 引例

先来看一组似乎矛盾的代码:

# 代码 1

>>> a = 3
>>> b = a
>>> b = 5
>>> a
3

这看上去似乎很好理解。第二步中, a 只是把值复制给 b,然后 b 又被更新为 5a 和 b 是两个独立的变量,那么 a 的值当然不会受到影响。

真的是这样吗?

再来看一段代码:

# 代码 2

>>> a = [1, 2, 3]
>>> b = a
>>> b[0] = 1024
>>> a
[1024, 2, 3]

第二步中,a 只是复制把列表复制给 b,然后更新 b[0] 的值,最后输出 a,可是 a 竟然也被改变了。

按照代码 1 的逻辑(即变量之间独立),代码 2 的中的 a 不应该受到影响。

为什么出现了这样的差异?

第二章 Python 的“反直觉”

先不解释上面那个“看似矛盾”的问题。

先来看看另一组简单的 Python 代码在内存中是什么样子的:

# 代码 3

b = 3
b = b + 5

它在内存中的操作示意图是这样的:

然而,从代码的的字面意思上看,“把 3 赋给 b,把 b 加 5 之后再赋给 b。”

也就是把代码看成这个样子:

b ← 3
b ← b + 5

所以下面这张在内存中的操作图可能更符合我们的直觉:

也即 b + 5 的值又写回到 b 中。典型的 C 程序就是这样的。为变量 b 分配一个 int 型的内存单元,然后将整数 3 存放在该内存单元中。b 就代表了该块内存空间,不再移动,可以更新 b 的值,但 b 在内存中的地址就不再变化了。所以我们说 b = b + 5,就等于 b ← b + 5,把 b 的值加 5 之后还依然放入 b 中。 变量 b 和它所在内存空间紧紧绑定在一起,人形合一。

而再看看上面 Python 中的内存示意图,b + 5 得到了一个新值,然后令 b 指向了这个新值。换句话说,它做的是事情是这样的:

b → 3
b → b + 5

先令 b 指向 3,再令 b 指向 b + 5 这个新值。

C 程序更新的是内存单元中存放的值,而 Python 更新的是变量的指向。
C 程序中变量保存了一个值,而 Python 中的变量指向一个值。

如果说 C 程序是通过操纵内存地址而间接操作数据(每个变量固定对应一个内存地址,所以说操纵变量就是操纵内存地址),数据处于被动地位,那么 Python 则是直接操纵数据,数据处于主动地位,变量只是作为一种引用关系而存在,而不再拥有存储功能。

在 Python 中,每一个数据都会占用一个内存空间,如 b + 5 这个新的数据也占用了一个全新的内存空间。

Python 的这种操作让数据成为主体,数据与数据之间直接进行交互。

而数据在 Python 中被称为对象 (Object)

这句话并不太严谨。不过在这个简单的例子中是成立的。

一个整数 3 是一个 int 型对象,一个 'hello' 是一个字符串对象,一个 [1, 2, 3] 是一个列表对象。

Python 把一切数据都看成「对象」。它为每一个对象分配一个内存空间。 一个对象被创建后,它的 id 就不再发生变化。

id 是 identity 的缩写。意为“身份;标识”。
在 Python 中,可以使用 id(),来获得一个对象的 id,可以看作是该对象在内存中的地址。

一个对象被创建后,它不能被直接销毁。因此,在上个例子中,变量 b 首先指向了对象 3,然后继续执行 b + 5b + 5 产生了一个新的对象 8,由于对象 3 不能被销毁,则令 b 指向新的对象 8,而不是用对象 8 去覆盖对象 3。在代码执行完成后,内存中依然有对象 3,也有对象 8,变量 b 指向了对象 8

如果没有变量指向对象 3(即无法引用它了),Python 会使用垃圾回收算法来决定是否回收它(这是自动的,不需要程序编写者操心)。

一个旧的对象不能被覆盖,因旧的对象交互而新产生的数据会放在新的对象中。也就是说每个对象是一个独立的个体,每个对象都有自己的“主权”。因此,两个对象的交互可以产生一个新的对象,而不会对原对象产生影响。在大型程序中,各个对象之间的交互错综复杂,这种独立性则使得这些交互足够安全。

C 程序为每个变量都分配一个了固定的内存地址,这保证了 C 变量之间的独立性。

C 语言是变量(也即内存地址)之间的交互,Python 是对象(数据)之间的交互。这是两种不同的交互方式。

那么,Python 这种数据之间直接进行交互的好处体现在哪里?

很遗憾,这并不是本文所要讨论的内容,该部分属于面向对象设计的核心内容。本文只是对 Python 的这种交互方式与 C 语言的交互方式做了一些比较,以区分两者在逻辑与物理上的差异所在。

相信这种逻辑会帮助你更好地编写 Python 程序,并且帮助你在日后更加深入地理解面向对象的程序设计。

本章补充:
Python 的赋值更改的是变量的指向关系,因此,对于 Python,从前向后阅读一个赋值表达式会更加容易理解。

// C 语言
b ← b + 5	// 把 b+5 的值赋给 b
# Python
b → b + 5	# 令 b 指向 b + 5

第三章 回答第一章的问题

先看代码 1:

# 代码 1

>>> a = 3
>>> b = a
>>> b = 5
>>> a
3

Python 中所有的数据都是对象,数字类型也不例外。3 是一个 int 类型的对象,5 也是一个 int 型的对象。
第一行,a 指向对象 3
第二行,令 b 也指向 a 所指向的对象 3
第三行,因为对象不可被覆盖(销毁),令 b 指向新对象 5,则只剩下 a 指向对象 3
第四行,输出 a,得到 3

在内存中的操作示意图 (Python):

这与第一章中的解释完全不同,第一章中的解释是用 C 语言解释的:

这是两种完全不一样的机制。

Python 中 b 首先指向了对象 3,然而因为对象之间的独立性,一个对象不能去覆盖另一个对象,则令 b 指向对象 5,而不是将对象 3 在内存中替换为对象 5

再来看代码 2:

# 代码 2

>>> a = [1, 2, 3]
>>> b = a
>>> b[0] = 1024
>>> a
[1024, 2, 3]

第一行,令 a 指向一个列表 [1, 2, 3]
第二行,令 b 也指向 a 所指向的列表;
第三行,令 b[0] = 10241024 虽然是一个对象,但它并没有试图覆盖b所指向的对象,而是对该对象的第一个元素进行修改。修改,而不是覆盖,所以它可以原对象进行操作,而不是令 b 指向修改后的对象。
所在第四行输出的 a 所指向的列表也发生了变化。

在内存中的操作示意图 (Python):

这种对象的值可以修改的对象被称为可变对象 (immutable object)。常见的列表、字典为可变对象。

因为它的值可以被修改,因此如果有多个变量指向该列表:

a = [1, 2, 3]
b = a
c = a
d = a
...

那么使用 b, c, d, ... 的任何一个变量都能访问该对象并修改其中的内容。这种特性常常被我们用于函数的参数传递,如果函数的参数是可变对象,那么函数可以对“实参”中的内容进行修改:

>>> a = [1, 2, 3]
>>> def change(t):
		t[0] = 1024

>>> change(a)
>>> a
[1024, 2, 3]
>>>  

调用函数 change 时,令 t 也指向了 a 所指向的列表,然后使用 t 更改了列表中的第一个元素,更改,而不是覆盖,因此对 t 所指向的对象的更改也改变了“实参” a 所指向的对象。而 C 语言则因为实参到形参是值传递,则无法改变实参的内容(虽然借助指针可以实现,但这里只说一般情况下)

但在函数以外的区域,我们要尽量避免这样使用,这很容易导致出错(当然,有时候会很有用,这取决于你的程序)。比如,在多人协作编程时,如果甲不小心修改了某可变对象,那么乙、丙、丁等用到该对象的人都会受到影响。

而对于不可变对象 (immutable object),即其值无法更改的对象,传入函数时则不会影响“实参”的值:

>>> a = 5
>>> def add(n):
		n = n + 2

>>> add(a)
>>> a
5

调用函数 add 时,令 n 也指向了 a 所指向的对象 5, 再执行 n = n + 2n 所指向的对象 5 与对象 2 相加得到了一个新的对象 7由于一个对象不能覆盖另一个对象,则 n 指向新的对象 7,而没有改变原对象。因此 a 的值未发生变化。虽然与 C 程序的结果一致,但与 C 程序的机制完全不同,C 程序之所以没改变 a,是因为调用函数时只发生了值传递,即只把 a 的值复制给了 n

不要混淆这两种赋值逻辑,它们有着完全不同的物理实现方式。

不同的思维逻辑会导致不同的编写逻辑。尽管这两种逻辑在很多情况下的结果是一致的,但并不能就简单地认为它们是一致的。否则在一些小的细节方面出了错误,就会难以理解。只能死记硬背,把一些东西当作 Python 的特例来记,虽然「唯手熟尔」也可以让你走得很远,但思维正确时,不仅可以走得更远,也会走得更加轻松。

比如,当你的思维清晰时,以下问题的答案自然也就水落石出了:

  • 为什么列表的方法的返回值大多是 None
  • 为什么字符串的方法的返回值大多是一个新的对象?
  • 为什么 Python 中没有自增/自减运算符?
  • 为什么有的可变对象传入函数之后,却不能被函数修改“实参”的值?
    (比如将上面的 change 函数的主体改成 t = t[1:]。调用函数之后,a 所指向的对象并没有发生改变。)
  • ……

这些内容与本文主题不大相关,所以不再列出答案。

有趣的补充:

1. 数字是一个天然的不可变对象(immutable object)。
对于 n = n + 2,有人可能会说,为什么不能把它看成像列表那样的修改,修改后 n 依然指向的是原对象,这样的话执行 add(a) 之后,a 就会变成 7 了,可为什么不是这样?
因为每一个数字都是一个单个的对象,而对象不能覆盖对象。所以该句实际上是: a 指向的对象加上对象 2,产生了一个新的对象,然后令 a 指向了新对象 a + 2
因此,数字类型并不存在修改这一说,它是一个天然的不可变对象。

2. 为什么 Python 中没有自增(++)、自减(--)运算符?
自增或自减运算符,在 C 语言中很常用,简洁实用。但在 Python 中却一定不会有。上节说到,数字是天然的不可变对象,所谓自增就是自身增加,所以它无法自增。它只能从一个对象指向下一个对象。可以这样写 a += 1
3. 既然 Python 更改的只是引用关系,那么如何复制一个列表?

a = [1, 2, 3]
b = a
# 这样做不能复制一个列表,a 和 b 指向的都是列表 [1, 2, 3]
# 答案:
## 1. 使用 list 的 copy 方法
b = a.copy()
## 2. 使用 slice 操作
b = a[:]	# slice 操作返回一个新的对象

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/360045.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Acer新蜂鸟Swift3电脑开机总是蓝屏错误怎么办?

Acer新蜂鸟Swift3电脑开机总是蓝屏错误怎么办?有用户使用的Acer新蜂鸟Swift3电脑一开机的时候,没过几秒电脑桌面就变成了蓝屏的了,通过强制重启之后依然会重复这个问题,那么这个问题要怎么去进行解决了,今天将你怎么重…

Android 反序列化漏洞攻防史话

Java 在历史上出现过许多反序列化的漏洞,但大部分出自 J2EE 的组件。即便是 FastJSON 这种漏洞,似乎也很少看到在 Android 中被实际的触发和利用。本文即为对历史上曾出现过的 Android Java 反序列化漏洞的分析和研究记录。 序列化和反序列化是指将内存数…

k8s-kubectl命令

文章目录一、kubectl 基本命令1、陈述式资源管理方法:2、声明式资源管理办法二、基本信息查看三、项目的生命周期创建kubectl run命令四、金丝雀发布(Canary Release)——陈述式管理方法五、声明式管理方法kubectl create 和 kubectl apply区别一、kubectl 基本命令 1、陈述式…

交叉验证 | 机器学习

1、交叉验证 1.1概念 交叉验证的核心思想:对数据集进行多次划分,对多次评估的结果取平均,从而消除单次划分时数据划分得不平衡而造成的不良影响。因为这种不良影响在小规模数据集上更容易出现,所以交叉验证方法在小规模数据集上更…

Profinet转ModbusTCP网关连接昆仑通态触摸屏配置案例

本案例是模拟将Modbus TCP 设备数据接入到西门子PROFINET 网络中。 使用设备为西门子 S7-1500 型 PLC, Profinet转ModbusTCP网关。MODBUS 从站昆仑通态触摸屏。 配置方法: 打开博图,新建项目并添加站点。 添加1513PLC。 设置好IP并处于联网状态 导入Pr…

【前端】小程序开发入门:安装开发工具、目录结构与项目配置

文章目录前期准备目录结构app.jsonpageswindow其他前期准备 开发小程序要先申请一个对应的AppID:微信小程序 (qq.com) 微信官方小程序开发文档:微信开放文档 (qq.com) 然后安装一个小程序开发工具: 选择稳定版: 安装后打开&…

组合由于继承

目录 前言: 1.什么是继承? 2.继承的劣势、问题? 3.组合相比继承有哪些优势? 4、如何判断该用组合还是继承? 参考资料 前言: 我们在平时日常开发设计的过程中,经常会有人提到一条经典的设…

音视频基础之封装格式与音视频同步

封装格式的概念 封装格式(也叫容器)就是将已经编码压缩好的视频流、音频流及字幕按照一定的方案放到一个文件中,便于播放软件播放。 一般来说,视频文件的后缀名就是它的封装格式。 封装的格式不一样,后缀名也就不一样。 比如&a…

MMPBSA结合自由能计算原理

MMPBSA结合自由能计算原理 计算结合自由能的方法有很多,例如,热力学积分(Thermodynamic Integration,TI)、自由能微扰(Free Energy Perturbation,FEP)、MM/PB(GB)SA、线性相互作用能…

安科瑞消防应急照明和疏散指示系统在城市隧道的应用分析

【摘要】:随着城市的发展,交通量越来越大,交通状况越来越复杂,城市隧道的修建也随之变多。当隧道照明正常时,隧道内路面有足够的照度,隧道中快速行驶的汽车,大部分司机不用打开车灯或只需打开车…

Swift 周报 第二十三期

前言 本期是 Swift 编辑组自主整理周报的第十四期,每个模块已初步成型。各位读者如果有好的提议,欢迎在文末留言。 欢迎投稿或推荐内容。目前计划每两周周一发布,欢迎志同道合的朋友一起加入周报整理。 勇敢是即便知道好结局不会每每降临在…

项目中异常信息的统一处理以及JSR03校验

在项目中,我们经常会对前端传过来的数据判断是否有一些错误,比如:id是否为空,传过来的名称是否合格,如果不符合我们通常会抛出异常,那么小的项目可能每次抛出异常也不是很麻烦,但是对于一个大型…

详解HashMap

目录 1.hash code 2.数据结构 3.初始化 4.存取 4.1.put 4.2.get 5.迭代 6.扩容 7.JDK1.7版本存在的问题 7.1.性能跌落 7.2.循环链表 8.散列运算 9.扰动函数 1.hash code hash code是使用hash函数运算得到的一个值,是对象的身份证号码,用于…

将项目从 SVN 迁移到 GIT

场景 项目开发中,项目原本是用的SVN,已经用了一年了,现在公司要抛弃SVN用Git,要求把SVN的代码直接搬过去Git,并保留之前的历史提交记录。 操作步骤 找到已经被svn管理的项目的根目录 WinFarm,右键 Git Ba…

手把手教你将微信小程序放到git上

背景 首先,要创建一个自己的git仓库,这里默认大家都能够自己创建了git仓库了。如果不会创建仓库的话,百度一下,很容易就能够创建了!(后续,如有不知道在哪里,怎么创建仓库的话&#…

群晖-第4章-Docker安装redis

群晖-第4章-Docker安装redis 本章介绍群晖docker安装redis的方法。如果你需要外网访问,可以参考我的群晖第1章。 参考 群晖使用 docker部署 Redis - 编程之家 Redis设置密码_惜惜然的博客-CSDN博客 在本地新建一个文本文件,命名为redis.conf&#xff…

GEE学习笔记 六十四:绿色中国报告(个人版)

2019年上半年在遥感圈里最火的一篇文章莫过于这篇《China and India lead in greening of the world through land-use management》(China and India lead in greening of the world through land-use management | Nature Sustainability),…

Idea搭建Spring5.3.x源码阅读环境

1. 概述 Spring是一个轻量级Java开源框架,在Java项目开发过程中已经离不开Spring全家桶了,包括Spring、SpringBoot、SpringCloud等,学习好Spring基础源码也有助于更好在项目中使用Spring相关组件,在学习源码前需要搭建好源码学习…

二进制部署K8S

目录 一、环境准备 1、常见的k8s部署方式 2、关闭防火墙 3、关闭selinux 4、关闭swap 5、根据规划设置主机名 6、在master添加hosts 7、将桥接的IPv4流量传递到iptables的链 8、时间同步 二、部署etcd集群 1、master节点部署 2、查看证书的信息 2.1 创建k8s工作目…

使用useReducer + useContext 代替 react-redux

一. 概述 在 React16.8推出之前,我们使用react-redux并配合一些中间件,来对一些大型项目进行状态管理,React16.8推出后,我们普遍使用函数组件来构建我们的项目,React提供了两种Hook来为函数组件提供状态支持&#xff…