CoderXL's Blog

Back

1. Mutable, Immutable, and +=#

四段代码:

a = 1
b = 2
c = a
c += b
print(a)
python
1
text

熟悉 C-like 语言的朋友会发现,Python 的 = 似乎表现得有时像引用传递,有时像值传递。

但实际上 Python 的建模完全不 care “引用”和“值”的概念。Python 的所有变量名都是对象的“名称”,而 = 的作用只是创建新的名称。

比如 c = a,表示给 a 绑定的对象取一个别名,叫作 c.
通过 a is c 可以验证两个名字是不是指向同一个对象。

但是,一些对象是只读的(Immutable),一些是可以修改的(Mutable)。

intboolstrtuple 等是 Immutable 的,而 listdictset 等是 Mutable 的。

对于 Immutable 的对象,c += b 的效果就是 c = c + b,而 c + b 是一个不同于旧 c 和旧 b 的新对象,并取名为 c,覆盖掉旧 c 的名字,因此新 c 就和 a 解绑了,表现为 c 变了但 a 没变;
对于 Mutable 对象,c += b 可以原地进行,不会创建一个新的 c + b 对象,因此 c 始终和 a 绑定到同一个对象,因此 c 修改之后 a 也变了。

2. Class Variables#

先看一个例子:

class Cls:
    a = 1

obj = Cls()
print(Cls.a, obj.a, Cls.a == obj.a, Cls.a is obj.a, obj.__dict__)

obj.a = 2
print(Cls.a, obj.a, Cls.a == obj.a, Cls.a is obj.a, obj.__dict__)
python
1 1 True True {}
1 2 False False {'a': 2}
text

发现向 obj.a 赋值会导致其从 Cls.a 中分裂出去,并且单独记录在 obj.__dict__ 中。

这是因为,obj 默认没有携带类成员属性 a,因此查询 obj.a 时会向上查找到 Cls.a
但是在写入时策略不同,当解释器发现 obj 没有名为 a 的属性时,会优先创建它,因此赋值之后 obj 获得了自己的 a,不再向上查找。


然而,这个设计却有一些反直觉的效果,比如将赋值换为 +=

class Cls:
    a = 1

obj = Cls()
print(Cls.a, obj.a, Cls.a == obj.a, Cls.a is obj.a, obj.__dict__)

obj.a += 1
print(Cls.a, obj.a, Cls.a == obj.a, Cls.a is obj.a, obj.__dict__)
python
1 1 True True {}
1 2 False False {'a': 2}
text

虽然结果相同,但是按照上面的原理,似乎无法解释。因为如果 obj.a 被当做 obj 自己的属性,那么由于 obj.a 还不存在,就无从进行加法;而如果 obj.a 被当做 Cls.a,那么结果里面为什么 Cls.a 没有增加,反而 obj.a = Cls.a + 1 了呢?

这个反直觉结果的本质原因是 Python 的 += 不是原子的,而是可以拆成三个步骤:

  1. tmp = obj.a,解释器发现 obj 不携带 a,因此向上找到 Cls.a 并读取,此时 tmp is Cls.a.
  2. tmp = tmp.__add__(1),由于 int 是 Immutable,解释器执行加法后重新绑定。加法后 tmp is not Cls.a.
  3. obj.a = tmp,解释器尝试对 obj.a 进行写入,发现 obj 不携带 a,创建属性,因此 tmp 的结果被转移到了 obj.a 中。

观察 obj.__dict__ 也可以知道,一开始 obj 没有自己的属性 a,尝试查询 obj.a 会返回 Cls.a,但是 += 之后 obj 就有了自己的 a.


另一个例子:

class Cls:
    a = [1]

obj = Cls()
print(Cls.a, obj.a, Cls.a == obj.a, Cls.a is obj.a, obj.__dict__)

obj.a += [1]
print(Cls.a, obj.a, Cls.a == obj.a, Cls.a is obj.a, obj.__dict__)
python
[1] [1] True True {}
[1, 1] [1, 1] True True {'a': [1, 1]}
text

这次的区别是,list 是 Mutable,因此 tmp 会采用原位加法,始终都有 tmp is Cls.a.

但是,在第三步赋值时,仍然不免创建一个 obj.a 属性,表现为 obj.__dict__ 中产生了 a 的记录。不过,这只是同一内存对象的两个名字,因为仍有 obj.a is Cls.a. 唯一的作用是此时查询 obj.a 将不会再向上查询到 Cls.a.

Python 颠覆认知的冷知识 #1
https://blog.leosrealms.top/blog/miscellaneous/2026-09-21-python-blows-your-mind-1
Author CoderXL
Published on 2026年9月21日
Comment seems to stuck. Try to refresh?✨