活动地址:CSDN21天学习挑战赛
本篇笔记记录了序列的+、*、+=、*=运算的使用以及细节。着重介绍了关于+=的一个谜题:t=(1,2,[3, 4]); t[2] += [50, 60],这条python语句会抛出异常,但是能够成功执行。
+、*运算Python程序员默认序列是支持+和*操作的。这两种运算都是非常简单的,所以不会过多介绍。
首先明确一点,对于+,*这类运算符,作用于序列时,都不会修改原序列,而是会新创建序列。即序列a,b。a + b和a * b,这两个表达式都不会修改a,b。
+运算用途:拼接序列。
示例:
a = [1, 2]
b = [i for i in range(10)]
c = a + b
print(c)
#结果为:
#[1, 2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
*运算用途:复制序列并将复制的元素拼接
示例:
a = [1, 2]
a2 = a * 2
print('a2=', a2)
b = [3, 4]
b3 = b * 3
print('b3=', b3)
#结果为:
#a2= [1, 2, 1, 2]
#b3= [3, 4, 3, 4, 3, 4]
这里提一下关于*运算作用于序列上时的陷阱。
*序列运算的陷阱有时,想要复制序列并拼接,最简单的方法就是使用*。
>>> a = [1, 2]
>>> a = a * 2
>>> a
[1, 2, 1, 2]
>>> a[3] = 100 #修改一个值
>>> a
[1, 2, 1, 100] #只有一个值被改动
以上代码简单易懂。但是如果我们操作的序列中的元素本身又是序列类型呢?
>>> a = [[1, 2], [3, 4]] #a是由两个列表构成的列表,简称为嵌套列表
>>> a = a * 2 #复制并拼接
>>> a
[[1, 2], [3, 4], [1, 2], [3, 4]]
>>> a[0][1] = 100 #修改一个值
>>> a
[[1, 100], [3, 4], [1, 100], [3, 4]] #有两个发生了改动,不符合预期
上述代码不符合预期。本来只是想改动一个值,但是却改动了两个。
根本原因是:列表是引用数据类型。
注:下面说明时,地址和引用没有区分开来,实际上二者在不同语言中有较大区别。但是地址和引用的作用有点类似,下面使用地址说明,更便于理解。实际实现过程,需要看Python源码。在流畅的Python书中,关于本章的内容,还只有介绍如何使用,相关实现在书后面的章节再做介绍。
同时,对于有过其他编程语言(比如C/C++)学习经历的读者,可以参看文末的两篇关于Python中引用的文章,能够加深理解。
比如:a = [1, 2]这句Python语句中,变量a中存储的并不是1,2这两个值,而是存放[1,2]的引用或者说是指向存储[1,2]的内存空间的地址。
所以,对于a = [[1, 2], [3, 4]],变量a指向的空间中实际存储的是两个地址,抽象一下,a = [地址值1,地址值2],那么在复制时,a = a * 2,Python会拷贝a指向的空间中的值,然后拼接到a的后面。所以,现在a 等于 [地址值1,地址值2, 地址值1, 地址值2]。a[0],a[2]指向相同的空间,因此当修改a[0]指向的空间时,a[2]指向的空间也就被修改了。
根据以上介绍,我们知道了,对于嵌套列表,使用*运算,会到达预料之外的效果。那么如何构建嵌套列表呢?或者说如何构建嵌套序列呢?
书中推荐的方法是:使用列表推导式。
>>> x = [[i] for i in range(10)]
>>> x
[[0], [1], [2], [3], [4], [5], [6], [7], [8], [9]]
>>> for i in range(10):\
... x[i][0] += 1
...
>>> x
[[1], [2], [3], [4], [5], [6], [7], [8], [9], [10]]
# 每个元素都加了1,说明列表x中每个列表元素都是互不相关的
+=``*=增量赋值运算符 += 和 *= 的表现取决于它们的第一个操作对象。
由于+=和*=的运算过程类似,只介绍+=。
+= 背后的特殊方法是
__iadd__(用于“就地加法”)。但是如果一个类没有实现这个方法的话,Python 会退一步调用__add__。也就是说,如果运算对象实现了
__iadd__,那么就调用该方法,这样该运算就转换成了对象的方法调用,对象会就地修改。如果运算对象没有实现__iadd__的话,a += b这个表达式的效果就变成了a = a + b,这时,就是调用__add__方法了,先计算a+b,然后将结果赋值给a。对于*=运算,其背后的特殊方法为__imul__,调用过程与+=基本一致,可以类推。
下面给出书中的例子,加深印象:
>>> l = [1, 2, 3]
>>> id(l)
4311953800 ①
>>> l *= 2
>>> l
[1, 2, 3, 1, 2, 3]
>>> id(l)
4311953800 ②
>>> t = (1, 2, 3)
>>> id(t)
4312681568 ③
>>> t *= 2
>>> id(t)
4301348296 ④
① 刚开始时列表的 ID。
② 运用增量乘法后,列表的 ID 没变,新元素追加到列表上。
③ 元组最开始的 ID。
④ 运用增量乘法后,新的元组被创建。
对不可变序列进行重复拼接操作的话,效率会很低,因为每次都有一个新对象,而解释器需要把原来对象中的元素先复制到新的对象里,然后再追加新的元素。但是str 是一个例外,因为对字符串做 += 实在是太普遍了,所以 CPython 对它做了优化。为 str 初始化内存的时候,程序会为它留出额外的可扩展空间,因此进行增量操作的时候,并不会涉及复制原有字符串到新位置这类操作。
+=谜团先看看下面这段代码,预测其运行结果:
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
如果你有了自己的预测,可以输入以上代码进行验证。
最后的结果是,解释器抛出异常,但是t被修改。即:
t 变成
(1, 2, [30, 40, 50, 60])。tuple不支持对它的元素赋值,所以会抛出TypeError异常。
结果:
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "" , line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
这里分析一下表达式s[a] += b的执行过程,对上面那个例子会有更好的理解。
这里笔者水平有限,暂时看不懂Python字节码。下面的例子源自书中。
>>> import dis
>>> dis.dis('s[a] += b')
1 0 LOAD_NAME 0 (s)
2 LOAD_NAME 1 (a)
4 DUP_TOP_TWO
6 BINARY_SUBSCR #①
8 LOAD_NAME 2 (b)
10 INPLACE_ADD #②
12 ROT_THREE
14 STORE_SUBSCR #③
16 LOAD_CONST 0 (None)
18 RETURN_VALUE
① 将 s[a] 的值存入 TOS(Top Of Stack,栈的顶端)。
② 计算 TOS += b。这一步能够完成,是因为 TOS 指向的是一个可变对象,也就是t[2]
③ s[a] = TOS 赋值。这一步失败,是因为s是不可变的元组。
不要把可变对象放在元组里面。
使用*作用于序列时,需要确保原序列中的元素不是引用类型。
应该通过列表推导式来构建嵌套序列
在有需要时,可以通过dis库来查看代码的字节码