Skip to content

高效Python90条之第11条 学会对序列做切片

Python有这样一种写法,可以从序列里面切片(slice)出一部分内容,让我们能够轻松地获取原序列的某个子集合。

最简单的用法就是切割内置的liststrbytes

# 切片操作:list
my_list = ["a", "b", "c", "d", "e", "f", "g", "h"]
print(my_list)
print(my_list[2:5])  # 获取从索引 2 到索引 5 的子列表(不包括索引 5)
# 输出:['c', 'd', 'e']


# 切片操作:str
my_string = "Hello, World!"

sub_string = my_string[0:5]  # 获取从索引 0 到索引 5 的子字符串(不包括索引 5)
print(sub_string)
# 输出:"Hello"

# 切片操作:bytes
my_bytes = b"Python is fun"
sub_bytes = my_bytes[7:10]  # 获取从索引 7 到索引 10 的子字节序列(不包括索引 10)
print(sub_bytes)
# 输出:b'is '

凡是实现了__getitem____setitem__这两个特殊方法的类都可以切片​。

class MySequence:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, index):
        # 支持切片操作
        return self.data[index]

    def __setitem__(self, index, value):
        # 支持设置切片
        self.data[index] = value

    def __repr__(self):
        return repr(self.data)

# 创建一个自定义序列对象
my_list = ["a", "b", "c", "d", "e", "f", "g", "h"]
my_seq = MySequence(my_list)

# 使用切片操作
print(my_seq[2:5])
# 输出:[30, 40, 50]

# 修改切片
my_seq[2:5] = [300, 400, 500]
print(my_seq)
# 输出:[10, 20, 300, 400, 500, 60, 70, 80, 90]

切片(slice)最基本的写法是用somelist[start:end]这一形式来切片,也就是从start开始一直取到end这个位置,但不包含end本身的元素。

a = ["a", "b", "c", "d", "e", "f", "g", "h"]

print("Middle two: ", a[3:5])
# 输出:Middle two:  ['d', 'e']

print("All but ends: ", a[1:7])
# 输出:All but ends:  ['b', 'c', 'd', 'e', 'f', 'g']

如果是从头开始切割列表,应该省略冒号左侧的下标0。如果一直取到列表末尾,应该省略冒号右侧的下标。用负数作下标表示从列表末尾往前算。

注意,用带负号的下标来切割列表,只有在个别情况下才会出现奇怪的效果。只要n大于或等于1somelist[-n:]总是可以切割出你想要的切片。只有当n0的时候,就需要特别注意。此时somelist[-0:]其实相当于somelist[0:],所以跟somelist[:]一样,会制作出原列表的一份副本。

# 从头开始切割列表
assert a[:5] == a[0:5]

# 一直切割到列表末尾
assert a[5:] == a[5:len(a)]

# 用负数做下标切割列表
print(a[:-1])  # 输出:['a', 'b', 'c', 'd', 'e', 'f', 'g']
print(a[-3:])  # 输出:['f', 'g', 'h']
print(a[2:-1])  # 输出:['c', 'd', 'e', 'f', 'g']
print(a[-3:-1])  # 输出:['f', 'g']

print(a[-0:])  # 输出:['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print(a[0:])  # 输出:['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
assert a[-0:] == a[0:]

如果起点与终点所确定的范围超出了列表的边界,那么系统会自动忽略不存在的元素。

print(a[:20])  # 输出:['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
print(a[:-20])  # 输出:[]
print(a[-20:])  # 输出:['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']

切割出来的列表是一份全新的列表。

print(id(a))  # 输出:123474874993472
print(id(a[:5]))  # 输出:123474874995328

切片赋值是一种操作,允许你用右侧的值替换原列表中指定范围内的元素。切片操作可以出现在赋值符号的左侧,表示用右侧的值替换原列表中位于该范围内的元素。

切片赋值与解包赋值的区别:

  • 切片赋值:
    • 不要求等号两边的元素个数必须相同。
    • 可以用右侧的值替换左侧切片范围内的所有元素。
    • 列表的长度可能会因为替换操作而发生变化。
  • 解包赋值:
    • 要求等号左侧的变量个数与等号右侧的值的个数一致。
    • 通常用于将一个序列中的值分配给多个变量。
    • 如果需要捕获剩余值,可以使用 *

示例1中,切片范围 [2:7] 覆盖了 5 个元素:["c", "d", "e", "f", "g"],右侧只有 3 个值:["X", "Y", "Z"],列表长度减少 2 个元素,因为右侧值少于左侧切片范围的元素个数。

示例2中,切片范围 [2:4] 覆盖了 2 个元素:["c", "d"],右侧有 4 个值:["X", "Y", "Z", "W"],列表长度增加 2 个元素,因为右侧值多于左侧切片范围的元素个数。

示例3中,等号左侧的变量个数(3 个:firstsecondthird)与等号右侧的值的个数(3 个:["a", "b", "c"])一致。如果将a[:3]改成a[:4],则报错ValueError: too many values to unpack (expected 3)

# 示例 1:用较少的值替换较多的元素
a = ["a", "b", "c", "d", "e", "f", "g", "h"]
a[2:7] = ["X", "Y", "Z"]  # 替换从索引 2 到索引 6 的元素
print(a)  # 输出:['a', 'b', 'X', 'Y', 'Z', 'h']

# 示例 2:用较多的值替换较少的元素
a = ["a", "b", "c", "d", "e", "f", "g", "h"]
a[2:4] = ["X", "Y", "Z", "W"]  # 替换从索引 2 到索引 3 的元素
print(a)  # 输出:['a', 'b', 'X', 'Y', 'Z', 'W', 'e', 'f', 'g', 'h']

# 示例 3:解包赋值
a = ["a", "b", "c", "d", "e", "f", "g", "h"]
first, second, third = a[:3]  # 解包前 3 个元素
print(first, second, third)  # 输出:a b c

把不带起止下标的切片放在赋值符号左边,表示是用右边那个列表的副本把左侧列表的全部内容替换掉(注意,左侧列表依然保持原来的id,系统不会分配新的列表)​。

a = ["a", "b", "c", "d", "e", "f", "g", "h"]
b = a
print(a)
print(b)
assert id(a) == id(b)  # 断言:a 和 b 是同一个对象,返回真

a = ["X", "Y", "Z"]
b = a
print(a)  # 输出:['X', 'Y', 'Z']
print(b)  # 输出:
assert id(a) == id(b)  # 断言:a 和 b 是同一个对象,返回真

要点:

  • 切片要尽可能写得简单一些:如果从头开始选取,就省略起始下标0;
  • 如果选到序列末尾,就省略终止下标。
  • 切片允许起始下标或终止下标越界,所以很容易就能表达“取开头多少个元素”​(例如a[:20])或“取末尾多少个元素”​(例如a[-20:0])等含义,而不用担心切片是否真有这么多元素。
  • 把切片放在赋值符号的左侧可以将原列表中这段范围内的元素用赋值符号右侧的元素替换掉,但可能会改变原列表的长度。