1. Mutable, Immutable, and +=#
四段代码:
a = 1
b = 2
c = a
c += b
print(a)python1texta = "1"
b = "2"
c = a
c += b
print(a)python1texta = [1]
b = [2]
c = a
c += b
print(a)python[1, 2]texta = (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)。
int,bool,str,tuple 等是 Immutable 的,而 list,dict,set 等是 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__)python1 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__)python1 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 的 += 不是原子的,而是可以拆成三个步骤:
tmp = obj.a,解释器发现obj不携带a,因此向上找到Cls.a并读取,此时tmp is Cls.a.tmp = tmp.__add__(1),由于int是 Immutable,解释器执行加法后重新绑定。加法后tmp is not Cls.a.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.