Python 1 - 内置类型 - 序列(1)
Python 中提供了 3 种基本的序列类型:list
、tuple
、range
。大家可能对这3中类型都比较熟悉。
一般我们认为 tuple
类型是不可以改变的list
,当然,这在日常使用中,并没有什么不对,表现出来的属性也能验证这个说法,但是,在Python
的底层实现当中,tuple
与 list
是完全不同的两个类型,后面我们会对他们的不同之处加以分析。
语句range(n)
大家在for in
语句中经常使用,一般作为index
来遍历其他类型的迭代。我们在使用的时候一般认为range(start, stop, step)
方法产生了一个以start
开始,stop
结束,以step
为间隔的列表。但是实际上,range
也是一种基本的序列类型,它也并不会返回一个我们认为的list
。
可变序列
不可变序列类型与可变序列的区别就是,可变类型没有实现对 hash()
内置函数的支持。这种对hash()
的支持,可以让 tuple
作为dict
的键存在。
像list
,bytearray
就是可变序列。
不可变序列
不可变序列类型的对象一旦创建就不能再改变:如果对象包含了对其他对象的引用,其中的可变对象就是可改变的;但是一个不可变对象所直接引用的对象集是不能改变的。
像 str
,tuple
,bytes
都是不可变对象。相应的不可变序列就有 tuple
和 range
,它们都是可以被hash()
所使用的。
关于可变对象和不可变对象官方给出的例子。
我们打算对元组的元素进行自增运算,所以我们使用了 +=
。
>>> a=(1,2)
>>> a[0] += 1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
报错信息告诉我们,元组对象不支持元素赋值。
发生异常的原因是显而易见的:
- 1 会与对象 a_tuple[0] 相加,而该对象为 (1),得到结果对象 2,
- 但当我们试图将运算结果 2 赋值给元组的 0 号元素时就将报错,因为我们不能改变元组的元素所指向的对象。
在表层之处,以上增强赋值语句所做的大致是这样:
>>> result = a_tuple[0] + 1
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
我们打算给元组中的列表元素进行扩展。
>>> a_tuple = (['foo'], 'bar')
>>> a_tuple[0] += ['item']
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
由于上面的例子,我们知道赋值会报错,但是当我们查看 a_tuple[0]
时发现,列表已经被改变了
>>> a_tuple[0]
['foo', 'item']
要明白为何会这样,你需要知道:
- (a) 如果一个对象实现了 iadd 魔术方法,它会在执行 += 增强赋值时被调用,并且其返回值将用于该赋值语句;
- (b) 对于列表来说,iadd 等价于在列表上调用 extend 并返回该列表。
因此对于列表我们可以说 += 就是 list.extend 的“快捷方式”:
>>> a_list = []
>>> a_list += [1]
>>> a_list
[1]
>>> result = a_list.__iadd__([1])
>>> a_list = result
a_list 所引用的对象已被修改,而引用被修改对象的指针又重新被赋值给 a_list。
赋值的最终结果没有变化,因为它是引用 a_list 之前所引用的同一对象的指针,但仍然发生了赋值操作
。
因此,在我们的元组示例中,发生的事情等同于:
>>>
>>> result = a_tuple[0].__iadd__(['item'])
>>> a_tuple[0] = result
Traceback (most recent call last):
...
TypeError: 'tuple' object does not support item assignment
iadd 成功执行,因此列表得到了扩充,但是虽然 result 指向了 a_tuple[0] 已经指向的同一对象,最后的赋值仍然导致了报错,因为元组
是不可变的。
序列的通用方法
所有的序列,都支持以下的方法,而每个方法都支持我们在自定义序列类型上实现这些操作。
in/not in
in 或 not in 是用来判断元素是否存在于序列当中。
>>> a = [1,2,3]
>>> 1 in a
True
>>> b = (1,2,3)
>>> 1 in b
True
>>> c = range(10)
>>> 1 in c
True
+
符号 +
是用来扩展序列的。而拼接不可变序列总是会生成新的对象。这意味着通过重复拼接来构建序列的运行时开销将会基于序列总长度的乘方。如果想降低开销,只能采用一些其他的方法。
>>> a = [1,2,3]
>>> a1 = [4,5]
>>> a+a1
[1, 2, 3, 4, 5]
操作 +
其实调用的是 __add__
或者 __radd__
。它们接收一个操作数,并根据是否是可变序列来返回一个新的对象或者更改自己的数据。
*
符号*
既+
的多次自身拼接。上面说了 __add__
是用来实现扩展的,那 *
实际调用的就是 __mul__()
或者 __rmul__()
。
>>> a = [1,2,3]
>>> a*3
[1, 2, 3, 1, 2, 3, 1, 2, 3]
>>> 3*a
[1, 2, 3, 1, 2, 3, 1, 2, 3]
这里其实会有一个在创建多维列表时会出现的问题,当你尝试创建一个二维列表时:[[1],[1],[1],[1]]
>>> a = [[1]] * 4
>>> a
[[1], [1], [1], [1]]
>>> a[0][0] = 2
>>> a
[[2], [2], [2], [2]]
我明明只给 a[0][0]
赋值了,为什么整个列表中其他子列表的值都改变了呢?
这是因为使用 *
创建列表时,执行重复操作并不是创建副本,而是创建对现有对象的引用。
所以,当我们创建多维列表时,使用列表推导式会更好。
>>> a = [[1] for i in range(4)]
>>> a
[[1], [1], [1], [1]]
>>> a[0][0] = 2
>>> a
[[2], [1], [1], [1]]
取 [i]
序列都支持从位置 i 处获取值,i 的范围是 [0,len(list))。
>>> a=[1,2,3]
>>> a[1]
2
[i] 操作调用的是 __getitem__(self, key)
方法,接收的键应为整数和切片对象。
而负数索引的特殊解读是取决于这个方法的。
切片操作
通过 range()
的切片操作,可以很容易的看出它们的关系。
>>> a = range(10)
>>> a[1:4]
range(1, 4)
>>> a[2:5:3]
range(2, 5, 3)
len, min, max, count
这些函数都是支持序列操作的。 len 既获取序列数据的长度,min 既获取最小值,max 既获取最大值,count 既用来获取序列中某个数据的个数。
可变序列类型的通用方法
l[i] = x
与取值 [i] 相对应的,可变序列的 [i] 可以被修改赋值。实际上调用的方法为 __setitem__
。
>>> a = [1, 2, 3]
>>> a
[1, 2, 3]
>>> a[0] = 2
>>> a
[2, 2, 3]
l[i:j] = x
与切片 [i:j] 相对应的,切片替换也是一样存在的。 l[i:j] = [i...j]。
注意:x 必须与它所替换的切片具有相同的长度,既len(x) == j-i
。
>>> a = [1,1,1,1,1,1]
>>> a
[1, 1, 1, 1, 1, 1]
>>> a[0:3] = [1,2,3]
>>> a
[1, 2, 3, 1, 1, 1]
del l[i:j]
等同于 l[i:j] = []
>>> a = [1,2,3]
>>> del a[0:1]
>>> a
[2, 3]
l.append(x)
将 x 添加到序列的末尾 (等同于 s[len(s):len(s)] = [x])
>>> a = []
>>> a.append(1)
>>> a
[1]
l.clear()
等同于 del l[:]
>>> a = [1,2,3,4]
>>> a.clear()
>>> a
[]
l.copy()
创建 s 的浅拷贝 (等同于 s[:]
) ,浅拷贝指的是通过 a 的值来构建一个新的对象 b,而 b 呢是跟 a 有着相同值得对象。
>>> a = [1,2,3]
>>> b = a.copy()
>>> b
[1, 2, 3]
>>> id(a)
4504616584
>>> id(b)
4504337160
s.extend(t) 或 s += t
用 t 的内容扩展 s (基本上等同于 s[len(s):len(s)] = t
)
>>> a = [1]
>>> b = [2,3]
>>> a+=b
>>> a
[1, 2, 3]
>>> a.extend(b)
>>> a
[1, 2, 3, 2, 3]
>>>
s.insert(i, x)
在由 i 给出的索引位置将 x 插入 s (等同于 s[i:i] = [x]
)。
>>> a = [1,2,3]
>>> b = [4,5]
>>> a.insert(0, b)
>>> a
[[4, 5], 1, 2, 3]
>>> a[0:0] = [b]
>>> a
[[4, 5], [4, 5], 1, 2, 3]
s.pop([i])
提取在 i 位置上的项,并将其从 s 中移除,返回被移除的值。
注意:可选参数 i 默认为 -1,因此在默认情况下会移除并返回最后一项。
>>> a
[[4, 5], [4, 5], 1, 2, 3]
>>> a.pop(1)
[4, 5]
>>> c = a.pop(1)
>>> c
1
s.remove(x)
删除 s 中第一个 s[i] 等于 x 的项目。
>>> a = [1, 2, 1, 2]
>>> a.remove(1)
>>> a
[2, 1, 2]
>>> a.remove(2)
>>> a
[1, 2]
s.reverse()
就地将列表中的元素逆序。当反转大尺寸序列时 reverse() 方法会原地修改该序列以保证空间经济性。 为提醒用户此操作是通过间接影响进行的,它并不会返回反转后的序列。
>>> a = [1,2,3,4]
>>> a
[1, 2, 3, 4]
>>> a.reverse()
>>> a
[4, 3, 2, 1]
后面的章节,将通过分析每个序列的使用与源码,进行解读。序列是 python 中非常重要的数据结构,了解序列的构成对深入学习 python 有十分重要的作用。
网友评论