Python底层不区分可变与不可变 运行时机制完全一致

【python】mutable和immutable其实根本没区别?带你了解这个概念背后你没思考过的东西 — 码农高天

码农高天

对象的不可变性并非源于Python虚拟机的类型约束,而是C底层封死所有纯Python修改接口的结果,在底层内存中所有对象本质上均可被修改。

AI-assisted summary of 【python】mutable和immutable其实根本没区别?带你了解这个概念背后你没思考过的东西 — original source. Check the original for full context.

Source published:

Key ideas

  • Python 虚拟机层面并不存在 mutable 与 immutable 的类型区分,两者纯属人类为了方便分类而人为创造的概念,运行时对它们的操作机制完全一致。 (0:00)
  • 常见的教学案例混淆了局部变量重新赋值与对象原地修改,且 Python 无论传递可变还是不可变对象,传参时都仅传递共享引用,绝不存在不可变对象的额外复制机制。 (1:45)
  • 不可变性并非由类型标签赋予,而是类型底层在实现时封死了所有通过纯 Python 修改其内存数据的交互接口;在 C 扩展视角下,所有 Python 对象本质上都是可被直接修改的内存块。 (5:00)

Detailed notes

## Background 该视频由频道“码农高天”于 2024 年 2 月 20 日发布在 YouTube 平台。视频简介指出,本期主要探讨 Python 中的 mutable(可变)与 immutable(不可变)对象,揭示 Python 底层对二者的操作完全一致、根本无法区分这一事实,并带领观众思考该概念背后更深层的内容。

## TL;DR 视频深入剖析了 Python 中“可变”与“不可变”对象的本质,指出这两个概念只是人类便于分类归纳的标签,Python 解释器本身既无法区分也没有对它们做差异化处理。作者逐一驳斥了教程中常见的“障眼法”反例,阐明运算符行为差异源于底层魔法方法的具体实现,且函数传参时始终传递引用而绝非复制对象。所谓不可变性,其实是 CPython 在底层用 C 语言封死了所有纯 Python 可修改内存的路径,一旦进入 C 扩展层面则万物皆可变;因此开发者应把这一概念当作沟通工具,遇到困扰时不妨像 Python 解释器一样对二者一视同仁。

## Atmosphere & style 整期视频呈现出一种直击本质、快节奏且辛辣犀利的技术辟谣风格。码农高天以极具穿透力的视角拆解常见误区,言辞直率不留情面,在痛斥某些错误教程“胡说八道”时带有鲜明的情绪起伏与说服力。伴随着层层递进的代码与底层机制推导,观看体验如同拨开层层概念迷雾,让人在恍然大悟之余感受到一种通透干脆的底层思维乐趣。

## Bulletin - [00:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=0s) Python 虚拟机层面并不存在 mutable 与 immutable 的类型区分,两者纯属人类为了方便分类而人为创造的概念,运行时对它们的操作机制完全一致。 - [01:45](https://www.youtube.com/watch?v=39vaZ93ICko&t=105s) 常见的教学案例混淆了局部变量重新赋值与对象原地修改,且 Python 无论传递可变还是不可变对象,传参时都仅传递共享引用,绝不存在不可变对象的额外复制机制。 - [05:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=300s) 不可变性并非由类型标签赋予,而是类型底层在实现时封死了所有通过纯 Python 修改其内存数据的交互接口;在 C 扩展视角下,所有 Python 对象本质上都是可被直接修改的内存块。

## Key takeaways - [00:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=0s) Mutable 和 immutable 并非计算机底层或 Python 语言原生理解的硬性概念,而是人类对对象行为归纳出的抽象分类标签。 - [00:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=0s) Python 语言层面无法量化、抓取或通过纯代码检验所谓的“内部状态”,运行时对所有对象采取完全一致的调用机制。 - [01:45](https://www.youtube.com/watch?v=39vaZ93ICko&t=105s) 教材中常用“函数内修改列表与修改整数”来对比可变性属于障眼法,混淆了局部变量重新赋值与对象内部方法的调用。 - [01:45](https://www.youtube.com/watch?v=39vaZ93ICko&t=105s) `+=` 运算符在 list 和 int 上的表现分歧,源于各自 `__iadd__` 方法的实现逻辑不同,而不是解释器识别了可变/不可变标签。 - [01:45](https://www.youtube.com/watch?v=39vaZ93ICko&t=105s) “传递 immutable 对象会复制一份”的说法完全错误,Python 传参始终传递对象的内存引用,通过 `id()` 可验证形参与实参完全一致。 - [05:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=300s) 元组等结构之所以不可变,是因为其底层的 `__setitem__` 等接口直接禁止了写入,而非 Python 运行时主动约束了元组的属性。 - [05:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=300s) 不可变对象的准确定义是“无法通过纯 Python 手段修改其内部数据”,其实质是通过底层实现把所有纯 Python 的修改入口全部封死。 - [05:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=300s) 真正的不可变性必须依赖 C 语言底层封装实现,纯 Python 代码无法构造出完全无法被篡改的绝对不可变对象。 - [05:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=300s) 在 CPython 底层 C 扩展开发视角下,所有对象都只是普通内存块,没有任何对象是不可变的,C 指针可任意篡改其数据。 - [05:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=300s) Mutable 和 immutable 仅应作为辅助思考的工具标签,当该概念引发逻辑困扰时,应回归 Python 底层行为一致的视角来理解程序。

## Detailed walkthrough ### [00:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=0s) Python 本身根本不区分 Mutable 与 Immutable

在 Python 学习过程中,`mutable`(可变)和 `immutable`(不可变)是经常被提及的概念,但许多人对其理解十分模糊。码农高天直言,如果你不知道它们之间到底有什么区别,“那就对了,因为他们俩本质上没有区别”。尽管书籍和官方文档中都充斥着对这两个术语的定义,但这并不意味着它们是计算机或 Python 语言原生理解的底层概念。这两个词纯粹是人类为了对不同的对象(object)进行归纳分类而人为创造出来的。

```mermaid flowchart TD accTitle: Python 处理对象与人类认知的差异 accDescr: 展示人类概念层与 Python 底层调用机制的不同处理路径

subgraph Human["人类视角(抽象分类)"] M["Mutable 对象(如 list)"] IM["Immutable 对象(如 int、tuple)"] end

subgraph Runtime["Python 虚拟机视角(统一处理)"] Call["直接调用对象底层对应的方法<br/>(如 __iadd__、__setitem__)"] Mem["内存中的 C 数据结构"] end

M -->|"无视标签"| Call IM -->|"无视标签"| Call Call --> Mem ```

Python 语言层面既不知道什么东西是 mutable,也不知道什么东西是 immutable,开发者也根本写不出一套纯 Python 代码来直接检验一个类是可变还是不可变。虽然官方或常见教程常将 immutable 对象定义为“内部状态(internal state)不能被改变”,但这种表述细究起来站不住脚:在 Python 中程序根本无法量化或抓取所谓的“内部状态”,它只是一个人脑容易理解、程序却无法解析的概念。码农高天强调:“Python是根本不会去分辨一个object是mutable还是immutable的,Python对于mutable和immutable的object行为是完全一致的。”

- 人类分类与语言底层:mutable 和 immutable 不是 Python 数据结构自带的语法标记,而是具体类的实现方式在客观上决定了它表现出来的特性。 - “内部状态”的模糊性:程序无法真正提取或定义一个对象的内部状态边界,因此该定义缺乏严格的操作性。

---

[01:45](https://www.youtube.com/watch?v=39vaZ93ICko&t=105s) 常见反例辨析:别用“障眼法”解释概念

针对“可变与不可变对象行为不同”的反驳,码农高天拆解了几个常见的典型例子。最粗浅的教学案例通常是:定义一个空列表 `L`(mutable)和一个整数 `A`(immutable),分别传入函数;在列表函数内部执行 `list.append(1)`,外部列表发生了变化;而在整数函数内部写 `int = 1`,外部变量没有变化。这种对比被码农高天斥为“睁着眼睛说瞎话”和经典的障眼法——两个函数调用的操作完全不同,一个是调用方法追加元素,另一个仅仅是局部变量重新赋值,行为当然不一样;若在函数内部把列表重新赋值为 `list = [1]`,外部同样毫无变化。

更为隐蔽的例子则是使用加等于运算符(`+=`):对一个 `list` 进行 `list += [1]`,对象在原地被修改了;而对整数 `i` 进行 `i += 1`,它并没有原地修改。表面上看两边写法一致,但背后的实质是二者 `+=` 对应魔法方法的底层实现截然不同。整数的 `+=` 实现返回了一个全新的 integer 对象,而列表的 `+=` 则是就地修改原本的列表并返回自身引用。针对这种混淆,码农高天反问:“难道因为我实现的这个类,它的加等于的行为和integer是一样的,它就是immutable了吗?不是这样的。”Python 在执行这些语句时,根本没有去识别类型标签,仅仅是机械、直接地调用了它们各自的实现。

针对传参机制的误解,部分教程声称“传递 mutable 时传的是同一个对象,而传递 immutable 时会复制一份对象”,码农高天直接给出四个字的评价:“胡说八道”。无论对象是可变还是不可变,函数传参时传递的都是同一个对象的内存地址。只要用 `id()` 函数分别打印外部传入的字符串(immutable)和函数内部形参的 `id`,就会发现数值毫无差异,完全不存在传参时额外复制的机制。

- 赋值与原地修改的混淆:局部变量重新绑定(`var =...`)与对象内部方法调用(`.append()`)不可同日而语。 - 运算符背后是方法实现:`+=` 的行为分歧源于类型内部 `__iadd__` 方法的实现逻辑,而不是类型被打上了“可变/不可变”属性。 - 传参实质:Python 始终采用共享对象引用的传递方式,传参阶段绝不会对 immutable 对象进行拷贝。

---

[05:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=300s) 底层实现、C 扩展与该概念的正确使用姿势

列表允许索引赋值(如 `L[0] = 1`)而元组(`tuple`)抛出异常,其根源同样不是 Python 预先识别了元组的不可变性,而是因为元组内部的 `__setitem__` 实现直接禁止了这项操作。所谓的 mutable 与 immutable,其核心定义必须加上严格的限定条件——“能不能通过纯 Python 来修改一个 object 里面的数据”。

在 CPython 虚拟机架构下,开发者在纯 Python 层面能调用的接口和手段是有限的。如果一个数据结构在底层用 C 语言实现时,把所有纯 Python 能够修改该结构内存的入口全部封死,它在 Python 视角下就变成了 immutable。码农高天强调其中的因果关系反差:“不是我实现的时候给这个数据贴个标签说这是immutable然后python看到这个标签就不改它了,不是这样的;是所有python有可能和这个数据交互的方式,我都在实现上,让他不要修改这个数据,然后他才成为了一个immutable的。”

这就引出了一个底层事实:所谓的“真不可变”只能依托于 C 语言层面的封装来实现,纯 Python 代码无法真正打造出一个有意义的完全不可变对象(虽然可以通过属性拦截等黑魔法增加篡改难度,但无法彻底封死修改途径)。而一旦脱离纯 Python 环境进入底层 C 扩展开发,由于所有对象本质上都是内存中可读写的数据块,整个 Python 数据体系将“没有任何东西是 immutable 的”,开发者可以用 C 指针改写任意想改的内存。

最后,码农高天总结道,厘清这套机制并不是要全盘否定 mutable 和 immutable 这对概念的价值。它们作为人类思考的抽象标签,能帮助程序员用一个词概括一组预期的行为特性。但大家不必对其产生教条式的认知负担:“当这个概念对你理解有帮助的时候使用它,当这个概念开始对你造成困扰的时候,你就可以像python一样认为他们俩是一样的。”

- 封死路径而非贴标签:不可变性是通过在底层杜绝所有纯 Python 交互修改路径来实现的,而不是运行时的规则约束。 - C 语言视角的穿透性:在 C 扩展层面,所有 Python 对象均直接映射为可变内存,不可变的神话在底层不复存在。 - 概念的工具属性:将可变性视为方便人类沟通的代码约定标签,当其产生逻辑困扰时,应回归 Python 底层行为一致的视角。

## Notable quotes > [00:00](https://www.youtube.com/watch?v=39vaZ93ICko&t=0s) Python是根本不会去分辨一个object是mutable还是immutable的,Python对于mutable和immutable的object行为是完全一致的。 — 码农高天

## Mentioned Python, CPython, C语言, 码农高天

## Video description 这期视频讲一下mutable和immutable,也就是可变对象和不可变对象。很多人可能压根没意识到,python对于mutable和immutable的操作是完全一致的,也就是python根本无法区分一个对象是mutable还是immutable。那这个概念背后到底有着什么值得思考的内容呢?

[Source](https://www.youtube.com/watch?v=39vaZ93ICko)

中文简报

Python底层不区分可变与不可变 运行时机制完全一致 对象的不可变性并非源于Python虚拟机的类型约束,而是C底层封死所有纯Python修改接口的结果,在底层内存中所有对象本质上均可被修改。 视频深入剖析了 Python 中“可变”与“不可变”对象的本质,指出这两个概念只是人类便于分类归纳的标签,Python 解释器本身既无法区分也没有对它们做差异化处理。作者逐一驳斥了教程中常见的“障眼法”反例,阐明运算符行为差异源于底层魔法方法的具体实现,且函数传参时始终传递引用而绝非复制对象。所谓不可变性,其实是 CPython 在底层用 C 语言封死了所有纯 Python 可修改内存的路径,一旦进入 C 扩展层面则万物皆可变;因此开发者应把这一概念当作沟通工具,遇到困扰时不妨像 Python 解释器一样对二者一视同仁。 Python 虚拟机层面并不存在 mutable 与 immutable 的类型区分,两者纯属人类为了方便分类而人为创造的概念,运行时对它们的操作机制完全一致。 (0:00) 常见的教学案例混淆了局部变量重新赋值与对象原地修改,且 Python 无论传递可变还是不可变对象,传参时都仅传递共享引用,绝不存在不可变对象的额外复制机制。 (1:45) 不可变性并非由类型标签赋予,而是类型底层在实现时封死了所有通过纯 Python 修改其内存数据的交互接口;在 C 扩展视角下,所有 Python 对象本质上都是可被直接修改的内存块。 (5:00)

Reading summary

码农高天指出,可变(mutable)与不可变(immutable)并非 Python 虚拟机底层能够识别的内置属性,而是人类为了归纳对象行为而人为设立的分类;在语言底层,Python 对两类对象的调用机制与传参行为完全一致。 概念源于人类抽象而非底层机制 (0:00) 在 Python 语言层面,虚拟机既无法识别对象是否可变,开发者也无法写出一套纯 Python 代码来直接检验一个类的可变性。教程与文档中常见的“内部状态不可改变”这一表述,在程序运行层面缺乏可量化、可抓取的边界。码农高天强调,Python 对可变与不可变对象的处理行为完全一致,遇到操作时仅机械调用其底层对应的方法,并不存在运行时针对分类标签的差异化对待。 澄清传参、赋值与运算符的认知误区 (1:45) 常见教学案例常将局部变量重新绑定与对象原地修改混为一谈。例如函数内修改列表与给整数赋值的差异,本质是调用对象方法追加元素与局部变量重新指向新地址的区别;若在函数内重新为列表变量赋值,外部同样不会发生变化。类似地,加等于(`+=`)运算符在列表与整数上表现不同,根源是两者底层实现的魔法方法逻辑不同(列表进行就地修改并返回引用,整数返回新对象),而非因为预先贴上了类型标签。此外,针对“不可变对象在函数传参时会被复制”的观点,高天指出这完全是误传,Python 传参始终采用共享对象引用的方式,通过 `id()` 函数即可验证传入前后的内存地址完全一致。 接口封死机制与 C 扩展下的本质 (5:00) 所谓的不可变性,实质上必须限定在“能否通过纯 Python 交互修改数据”的范畴内。例如元组禁止索引赋值,是因为其底层的 `__setitem__` 方法直接拒绝了修改,因果关系在于“底层实现封死了所有纯 Python 能修改该结构的入口”,而非“因为是不可变对象所以系统禁止修改”。纯 Python 层面无法彻底杜绝修改漏洞以构造完全意义上的不可变对象,而这一神话在底层 C 扩展面前同样不复存在——在 C 语言视角下,所有 Python 对象都是内存中可读写的数据块,开发者通过指针可以修改任意对象的内存内容。 码农高天总结指出,厘清底层逻辑并非要全盘否定这对概念的人类抽象价值;开发者应将其视为沟通与理解的辅助标签,但在概念带来教条式困扰时,应回归 Python 底层行为本就一致的事实。

Source quotes

[0:00] 码农高天: Python是根本不会去分辨一个object是mutable还是immutable的,Python对于mutable和immutable的object行为是完全一致的。

Original transcript

[0:00] 这期视频我们来聊一个经常被提到的话题叫做Muteable和Immutable很多人会对这两个概念很模糊啊不知道他俩有什么区别在这里我想郑重的告诉大家你如果不知道他们俩有什么区别那就对了因为他们俩本质上没有区别那肯定有人听到这就要强烈反对了对吧说哎不对不对对吧我看过这本书上说他来有什么区别哎我看过这个文档里也说这个muteable是什么immuteable是什么大家先不要着急啊我说他们两个本质上没有区别 并不是说这两个词是一个意思而是这两个词实际上是人为的被创造出来用来对不同的object进行分类的它并不是一个直接的编程上的概念换句话说 Python是不知道什么东西是mutable 什么东西是immutable的你也没有办法写一段python程序来判断一个类是mutable的还是immutable的而在我们说一些结构是mutable 或者immutable的时候并不是我们在写程序的时候定义了这个数据结构是mutable的或者是immutable的而是我们实现这个数据结构或者说这个类的方式决定了它是mutable的还是immutable的那 Python 对于 Immutable Object定义使它的 Internal State 不能被改变或者说它的内部状态是不可变的这个定义乍一听很有道理很清晰可是如果你细琢磨它就有点不对劲了因为内部状态是一个比较模糊的描述你在python里怎么能拿到一个数据结构或者一个类的内部状态呢它是一个人类容易理解而程序无法理解的人造的概念而事实上 就像我刚才说的 Python是根本不会去分辨一个object是mutable还是immutable的Python对于mutable和immutable的object行为是完全一致的那听到这肯定就有人不屑一顾了 对吧 说来来来 我给你举几个例子 告诉你multipal跟multipal有什么区别不用劳年大驾 我们就先来看看几个最常见的去分辨mutable和immutable的例子我们看左边是一个经典例子 L是一个空的list 我们说它是mutable的而A是一个Integer 我们说它是immutable的然后有人就会告诉你 你看你把list传进这个函数 然后你在list append1这个list a就变了然后你把这个integ传进这个函数 然后让这个integ=1 这个wem的integ就没变所以multiple跟immutable是不一样的这就属于睁着眼睛说瞎话你那函数里面做的事一样吗对吧 你函数里面一个用的是append 一个用的是等号 你函数都不一样 它behaviour当然不一样了你把这个F函数里面变成list等于方块1你看它外面变吗它肯定不变啊这就属于经典的障眼法他试图去告诉你muto跟emuto的东西啊他的行为是不一样的但实际上是他自己写的函数都不一样相比于刚才那个例子呢这个例子就隐蔽一些他说你看这和两个函数一样了吧一个是list加等于liste另外一个是爱加等于一你看结果这个mute object变了 这个immute没变确实是mute变了以mute没变他俩看似也长得一样但是他俩长得一样就调用的同一个函数了吗他们两个行为不一致 是因为他们两个加等于这个函数的实现不一致integer的加等于返回了一个新的integer而list+=改变了原来的list同时返回list本身是他们的加等于函数实现不一样 导致了他们的行为不一样 而不是因为他们俩一个是mutable 一个是immutable我完全可以实现一个Mutual的Object 让它的加等于返回一个新的Object难道因为我实现的这个类 它的加等于的行为和integer是一样的 它就是immutable了吗不是这样的在这个程序背后python根本就不会去分辨他们俩谁是muteable谁是muteable而是直接去调用他们的加等于函数那有关参数的传递 可能你会看到有的教程跟你说说mute传的是同一个object以 mutable是复制一份的object 对吧都听过这种说法吧给他四个大字啊胡说八道无论是mutable还是emutable 传的都是同一个object我们看左边代码我们随便建立了一个list然后建立一个string大家都知道string是一个immutable的东西对不对我们打印这个object id然后在这个函数里面我们再打印给入参数ID你可以看到ID是没有变化的根本不存在说immutableobject在传餐的时候复制了一份 没有这回事啊那面对这种经典的list与tube的行为不一致 大家肯定也都知道是怎么回事了 [5:00] 并不是因为tuple是immutable而是因为tuple这个setItem函数跟这个listsetItem函数不一样Python并不是因为知道tuple是一个immutableobject所以不允许给方括号0复制而是tuple这个setItem函数规定了它不允许这么给方括号0复制所以说在python中mute和emute是人为创造的帮助你理解的一个概念它的意思是你能不能通过纯python来修改一个object里面的数据这里注意我强调了纯python由于python虚拟机的结构导致了你可以对一个object进行的操作是有限的换言之只要一个数据结构在C层面实现的时候把所有你可以从python层面修改这个数据的路都封死那这个结构或者说这个class就变成了immutable的注意这中间细微的差别啊不是我实现的时候给这个数据贴个标签说这是immutable然后python看到这个标签就不改它了不是这样的是所有python有可能和这个数据交互的方式,我都在实现上,让他不要修改这个数据然后他才成为了一个immutable的那前面我也强调了,这个mutable和immutable概念是在纯python层面的一旦进入到c了比如说你写一个cextension那所有的python数据结构就没有emute的了它本质上都是在内存里面可修改的一个数据你可以修改你任何想改的东西同样的所谓的真emute也只有可能在c的层面实现你没有办法用纯Python实现一个有意义的immutable的object你可以通过一些技巧让这个object不那么容易被修改但是你永远没有办法在纯Python的层面上让它不能被修改讲了这么多当然不是告诉大家说mutable跟immutable是无用的概念它们之所以会被发明就是因为它们是有助于你理解Python的它像一个标签当一个数据被贴上了immutable的标签之后它就可以用这一个词来描述很多它的行为你在写程序的时候就可以依赖这些行为这期视频的目的是要告诉大家mutable和immutable它们在本质上是没有区别的所以大家在写程序的时候不用纠结于这两个概念当这个概念对你理解有帮助的时候使用它当这个概念开始对你造成困扰的时候你就可以像python一样认为他们俩是一样的好那这期视频就到这里希望大家有所帮助喜欢的话记得点一下关注别忘了一键三连哦