通过序列切片,我们可以轻松地访问由序列中的某些元素所构成的子集。比如常见的:list、str 和 bytes 序列切片;而且序列切片操作还可以扩展到实现了 __getitem__
和 __setitem__
这两个特殊方法的 Python 类上使用。
下面我们以列表的切片为例,介绍基本切片和步进式切片,以及使用切片操作时应该注意的事项。
一. 基本切片 - list[start:end]
熟悉 Python 的同学相比都知道,切片结果包含 start 索引位置的元素,不包含 end 索引位置的元素:
>>> my_list = list(range(10))
>>> my_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> my_list[4:6]
[4, 5]
当 start 索引为 0 ,或 end 索引为序列长度时,应当省略不写,避免代码冗余:
>>> my_list[:4]
[0, 1, 2, 3]
>>> my_list[-4:]
[6, 7, 8, 9]
上面在获取列表后四个元素时,使用了反向索引 -4 ,表示从列表尾部 -1 向前算,如:
>>> my_list[4:-4]
[4, 5]
序列切片时,不会计较 start 和 end 索引是否越界,利用这一特点,我们可以很轻易地限定输入序列的最大长度,即只读取该序列开头的 n 个值或末尾的 n 个值:
>>> first_20 = my_list[:20]
>>> first_20
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> last_20 = my_list[-20:]
>>> last_20
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
但请注意,访问列表的单个元素时,下标不允许越界,否则会抛出 IndexError
异常:
>>> my_list[20]
---------------------------------------------------------------------------
IndexError Traceback (most recent call last)
<ipython-input-11-36ca79ccfead> in <module>
----> 1 my_list[20]
IndexError: list index out of range
对原列表切片之后,会产生另外一份全新的列表,在切片后得到的新列表上进行修改,不会影响原列表。省略 start
end
,使用 [:]
时将使用浅拷贝产生一份原列表的拷贝:
>>> new_list = my_list[:]
>>> new_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> assert new_list == my_list and new_list is not my_list
>>> new_list[-1] = 9000
>>> new_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9000]
>>> my_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
通过断言可知,new_list
和 my_list
在数值上完全等同,但指向的却是两个不同的对象。 对 new_list
修改也不会影响 my_list
。
下面这个例子,我们作为和上个例子的对比学习,顺带讲解切片赋值。在赋值时对左侧列表使用切片操作,可以使列表中处在指定范围内的对象替换为新值。并且与元组的赋值不同,切片的长度无需和新值的个数相等,整个列表会根据新值的个数相应地扩张或收缩。
>>> new_quote = my_list
>>> new_quote
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> my_list[4:6] = [400, 500, 600, 700, 800, 900, 1000]
>>> my_list
[0, 1, 2, 3, 400, 500, 600, 700, 800, 900, 1000, 6, 7, 8, 9]
>>> new_quote
[0, 1, 2, 3, 400, 500, 600, 700, 800, 900, 1000, 6, 7, 8, 9]
>>> assert new_quote is my_list and new_quote == my_list
>>>
创建一个新的引用 new_quote
指向列表 my_list
,之后通过切片赋值,将 my_list
索引位置 4
和 5
的值替换为 [400, 500, 600, 700, 800, 900, 1000]
,my_list
和 new_quote
变量的访问结果都发生了改变,因为他们本身指向的就是同一个列表。
二. 步进式切片 - list[start:end:stride]
除了基本的切片操作,Python 还提供了 list[start:end:stride]
形式的写法,支持步进切割:
>>> my_list = list(range(10))
>>> my_list
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> my_list[::2]
[0, 2, 4, 6, 8]
>>> my_list[1::2]
[1, 3, 5, 7, 9]
通过上面的例子,我们很容易发现,步进切割其实就是每 stride
元素取一个。此外 stride
也可以是负数,表示反向切割,即倒着来。如:Python 中有一种常见的技巧,利用 -1
做步进值,能够把以字节形式存储的字符串翻转过来:
>>> 'python'[::-1]
'nohtyp'
>>> b'python'[::-1]
b'nohtyp'
>>> '中国'[::-1]
'国中'
注意,上述例子中,我们使用 [::-1]
翻转序列时,Unicode字符序列、只包含 ASCII 字符的字节串翻转都是没问题的。但下面的这个例子就出现意外了:
>>> china = '中国'.encode('utf-8')
>>> china
b'\xe4\xb8\xad\xe5\x9b\xbd'
>>> china[::-1]
b'\xbd\x9b\xe5\xad\xb8\xe4'
>>> china[::-1].decode('utf-8')
---------------------------------------------------------------------------
UnicodeDecodeError Traceback (most recent call last)
<ipython-input-15-ca216891a42e> in <module>
----> 1 china[::-1].decode('utf-8')
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xbd in position 0: invalid start byte
包含非 ASCII 字符的字符串被编码为字节串之后,其实是由 3 个字节共同来完成一个汉字的存储。因此反转之后,本来 b\xe5\x9b\xbd
表示的 国
,使用 b\xbd\x9b\xe5
解码,自然会报错。
此外,尽量不要在切片操作时同时指定 start
stop
stride
,因为这样的写法不仅看上去太紧密,而且也不易读:
>>> my_list[2::2]
[2, 4, 6, 8]
>>> my_list[-2::-2]
[8, 6, 4, 2, 0]
>>> my_list[-2:2:-2]
[8, 6, 4]
>>> my_list[2:-2:-2]
[]
如果程序对时间和空间要求不是很苛刻的情况下,建议可以将步进制切片和范围切片分两步进行。只是这样会多产生一份拷贝,即中间变量 tmp
:
>>> tmp = my_list[::2]
>>> new_list = tmp[1:-1]
>>> new_list
[2, 4, 6]
如果必须同时指定 start
stop
stride
,建议使用 itertools
模块的 islice
函数完成:
>>> from itertools import islice
>>> iter_slice = islice(my_list, 2, 7, 2)
>>> list(iter_slice)
[2, 4, 6]
注:
iterools
模块的islice
方法,不允许start
stop
step
为负值。