Python列表与元组的思考

导论

首先,举一个例子:

a = [1, 2, 3]
b = (1, 2, 3)
print(id(a)) # To print the identity of a
print(id(b)) # To print the identity of b
a += [4, 5] # a = a + [4, 5]
# That is, a.extend([4, 5])
b += (4, 5) 
print(a)
print(b)
print(id(a)) # To print the identity of a
print(id(b)) # To print the identity of b

程序的运行结果如下所示(id取决与特定的运行环境)。

139996770046400
139996770056448
[1, 2, 3, 4, 5]
(1, 2, 3, 4, 5)
139996770046400
139996770061184

实际上我们只进行了一个十分简单的操作,对于列表而言,我们可以发现连接的操作是in-place的,因为操作前后a都是指向了该列表,严格来说,引用没有发生变化。

对于元组而言,我们发现操作前后引用是发生了变化的,因为元组是不可变的列表,对元组进行连接的过程,实际上创建了一个新的元组,这个操作不是in-place的。然后将b指向了这个新的元组,至于原先的元组(1, 2 ,3)的回收就交给垃圾回收机制。

从上述的讨论,我们可以得出一个重要的结论:

元组应保持其应有的特性,不应该尝试去更新元组,因为这样的更新往往是不高效的。对于列表而言,更新是in-place的。

元组里面的列表

问题的叙述

下面的讨论基于《Fluent Python》1的第二章中的《Augmented assignment with sequences》。

我们看下面的例子:

t = (1, 2, [30, 40])
t[2] += [50, 60]

我们给出四个答案:

  • t 变成 (1, 2, [30, 40, 50, 60])
  • 会产生TypeError的异常
  • 上述都不发生
  • 上述都会发生

从经验上以及直观上,你或许认为会产生TypeError的异常,但实际上正确的答案应该是第四个:上述都会发生。

尝试在IDLE运行Python程序如下图所示:

Python列表与元组的思考_第1张图片
产生了异常,但是t的值也发生了变化,我们借助一个工具分析2,对上述的Python代码可视化:

Python列表与元组的思考_第2张图片
我们可以看到,t 指向了一个元组,而元组的第二个元素指向了一个列表。然后我们执行下一步操作:

Python列表与元组的思考_第3张图片

我们可以看到,产生了异常,同时列表的值发生了变化。

问题的解释

问题出在哪个问题,在于+=这个运算符,从上述的讨论过程可以得知,+=的操作并不是原子式的,对于表达式t[2] += [50,60] 首先执行的是t[2] + [50, 60],这一步是可以执行的,因为t[2]指向的是一个列表,列表是可变的,但是t[2]是元组的一个元素,是不允许改变的,也就是不可以进行赋值的。但是t[2]的值发生了变化。

问题的结论与反思

从上述的讨论中,我们可以得出一个十分重要的结论 += 并不是原子式的操作,产生了异常却修改了元组元素的值。但更重要的是,我们应该意识到:

元组不应该包含列表,元组应保持其不变的特性,否则会产生Bug。

后记

本思考基于对《Fluent Python》的学习,作者在此也只是抛砖引玉罢了,对问题的讨论或许也是不够深刻,若有不对的地方,及表达不清欠妥的地方,还请各位谅解。

参考


  1. Ramalho L. Fluent python: Clear, concise, and effective programming[M]. " O’Reilly Media, Inc.", 2015. ↩︎

  2. Python代码执行可视化web工具 ↩︎

你可能感兴趣的:(Python列表与元组的思考)