Срезы#

Срезы встроенных коллекций python#

Простые срезы#

Списки, строки, кортежи и другие последовательности python кроме обычной индексации поддерживают индексацию срезами (slices). Рассмотрим на примере строк.

import string

s = string.ascii_lowercase
print(s)
abcdefghijklmnopqrstuvwxyz

Итак, у нас есть строка a, состоящая из строчных символов английского алфавита.

Если мы хотим извлечь подстроку, начиная с позиции start включая и заканчивая позицией stop не включая, то применяется синтаксис

sequence[start:stop]

Например, если нас часть строки с 1-го по 5-ый символ (нумерация с нуля), мы запишем

print(s[1:5])
bcde

Если нас интересует подстрока с самого начала строки до какого-то символа с индексом stop (не включая), то можно опускать первый параметр среза, т.е. допускается синтаксис

sequence[:stop]

Следующие два среза синонимичны и оба возвращают первые три символа.

print(s[0:3])
print(s[:3])
abc
abc

То же самое справедливо и про второй параметр: если нас интересует подстрока до самого конца исходной строки, начиная с символа с индексом start (включая), то можно опустить второй параметр, т.е. допускается синтаксис

sequence[start]

Следующие два среза синонимичны и оба возвращают символы с 23-го по конце.

print(s[23:len(s)])
print(s[23:])
xyz
xyz

Допускается даже опустить оба параметра. Тогда возвращается копия строки.

print(s[:])
abcdefghijklmnopqrstuvwxyz

Если один или оба параметра выходят за пределы последовательности, то это не является формальной ошибкой. Результатом будет та часть строки, что пересекается с выбранным срезом, которая может оказаться пустой, если индексы среза не пересекаются с допустимыми индексами строки.

# символы '|' по бокам, чтобы наглядно показать края строки
print(f"|{s[-100:100]}|")
print(f"|{s[90:100]}|")
print(f"|{s[5:2]}|")
|abcdefghijklmnopqrstuvwxyz|
||
||

Срезы с шагом#

Для разнообразия теперь будем брать срезы от списков.

L = list(range(10))
print(L)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Итак, у нас есть список L чисел от 0 до 9.

Срезы с шагом позволяют извлекать элементы не подряд, а, например, с прореживанием. Для этого предыдущий синтаксис расширяется третьим параметром, который как раз и отвечает за шаг.

sequence[start:stop:step]

Например, чтобы вырезать каждый второй элемент списка L, начиная с 1-го и заканчивая 8-м (не включая), необходимо записать.

print(L[1:8:2])
[1, 3, 5, 7]

Как и прежде, первые два параметра можно опускать, если start и stop совпадают с началом и концом списка соответственно.

Ниже берется каждый второй элемент всего списка.

print(L[::2])
[0, 2, 4, 6, 8]

Шаг может быть отрицательным, но тогда start должен быть больше step, чтобы вернулась не пустая подпоследовательность. При этом, как и до этого, start попадает в срез, а stop нет.

print(L[8:1:-1])
[8, 7, 6, 5, 4, 3, 2]

Получить последовательность в обратном порядке (обратить последовательность) можно синтаксисом

sequence[::-1]
print(s[::-1])
print(L[::-1])
zyxwvutsrqponmlkjihgfedcba
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Присваивание по срезам#

Если последовательность изменяемая (список, например), то допускается присваивание по срезу.

Если шаг не указан, то можно заменить элементы среза в списке элементами коллекции справа от оператора “=” вне зависимости от того, совпадает ли количество элементов в коллекции справа от оператора “=” с размером среза слева от оператора “=”.

print(L)
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

Например, код в ячейке ниже заменяет первые два элемента списка L на новые два элемента (0 заменяется на 3.14, 1 заменяется на 42).

L[:2] = [3.14, 42]
print(L)
[3.14, 42, 2, 3, 4, 5, 6, 7, 8, 9]

Код в ячейке ниже заменяет первые два элемента (3.14 и 42 после предыдущей замены) на сразу три новых элемента, тем самым расширяя список.

L[:2] = [0, 3.14, 42]
print(L)
[0, 3.14, 42, 2, 3, 4, 5, 6, 7, 8, 9]

Следующим синтаксисом можно удалить первых два элемента.

L[:2] = []
print(L)
[42, 2, 3, 4, 5, 6, 7, 8, 9]

Хотя для этого предпочтительнее воспользоваться синтаксисом

del L[:2]

Если шаг указан, то количество элементов в коллекции справа от “=: должно совпадать с количеством элементов в срезе

L = list(range(10))

# Oк
print(L[::2])
L[::2] = L[1::2]

# Ошибка
print(L)
L[::2] = []
[0, 2, 4, 6, 8]
[1, 1, 3, 3, 5, 5, 7, 7, 9, 9]
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In [16], in <cell line: 9>()
      7 # Ошибка
      8 print(L)
----> 9 L[::2] = []

ValueError: attempt to assign sequence of size 0 to extended slice of size 5

Срезы массивов NumPy#

Срезы массивов NumPy введут себя очень похоже с двумя основными отличиями (документация NumPy про срезы):

  1. при присваивании по срезу размер массива не может измениться вне зависимости от того, указан шаг или нет;

  2. допускается делать многомерные срезы.

Срезы одномерных массивов#

Если массив одномерный, то роль играет только первое отличие.

import numpy as np
array = np.arange(10)
print(array)
[0 1 2 3 4 5 6 7 8 9]
print(array[::2])
[0 2 4 6 8]
array[::2] = [9, 7, 5, 3, 1]
print(array)
[9 1 7 3 5 5 3 7 1 9]

Многомерные срезы#

Но если массив многомерный, то можно делать срезы сразу вдоль нескольких осей. Рассмотрим на примере двухмерных массивов.

matrix = np.arange(1, 26).reshape(5, 5)
print(matrix)
[[ 1  2  3  4  5]
 [ 6  7  8  9 10]
 [11 12 13 14 15]
 [16 17 18 19 20]
 [21 22 23 24 25]]

Обратим внимание, что выражением matrix[i, j] мы получаем элемент массива matrix на пересечении i-й строки и j-й строки.

print(matrix[2, 2])
13
../../_images/slices_0.svg

Такая же логика обобщается и на срезы. Например, получить j-й столбец двухмерного массива matrix можно выражением

matrix[:, j]

То есть указать вдоль первой оси (axis=0) указывается полный срез (все строки), а вдоль второй оси (axis=1) указывается число j (только j-й столбец). Получим таким образом нулевой столбец.

print(matrix[:, 0])
[ 1  6 11 16 21]

Итого, процесс получения двухмерного среза можно мысленно представить в следующем виде:

  • срез, указанный до запятой, вырезает строки из массива;

  • срез, указанный после запятой, вырезает столбцы из массива;

  • результирующий срез представляет собой все элементы, что располагаются на пересечении этих строк и столбцов.

../../_images/slices_1.svg

Одну i-ю строку можно получить как выражением matrix[i, :], так и выражением matrix[i]. Рассмотрим более содержательные примеры.

Так, например, чтобы получить элементы на пересечении первых двух строк и столбцов с первого (включая) по четвертый (не включая), необходимо применить следующий срез.

matrix[:2, 1:4]
array([[2, 3, 4],
       [7, 8, 9]])
../../_images/slices_2.svg

Если требуется получить каждый второй столбец, то вдоль первой оси (axis=0) указывается полный срез (все строки), а вдоль второй оси (axis=1) указывается полный срез с шагом 2 (каждый второй столбец).

matrix[:, ::2]
array([[ 1,  3,  5],
       [ 6,  8, 10],
       [11, 13, 15],
       [16, 18, 20],
       [21, 23, 25]])
../../_images/slices_3.svg

В качестве последнего примера рассмотрим получение срезом элементов из каждой второй строки и каждого второго столбца.

matrix[::2, ::2]
array([[ 1,  3,  5],
       [11, 13, 15],
       [21, 23, 25]])
../../_images/slices_4.svg

Присваивание по срезу#

Срезы ссылаются на данные исходного массива. По ним можно производить присваивание. Например, можно целиком заменить столбец матрицы.

matrix[:, 0] = np.arange(-5, 0)
print(matrix)
[[-5  2  3  4  5]
 [-4  7  8  9 10]
 [-3 12 13 14 15]
 [-2 17 18 19 20]
 [-1 22 23 24 25]]

В примере выше формы среза слева от “=” и массива справа от “=” совпадают. Это необязательное условие: работают правила броадкастинга, т.е. массив справа от оператора “=” может быть расширен до размера среза слева от оператора “=”, если их формы совместимы и срез сможет поместить в себе массив по всем измерениям. Например, прибавим к всем элемента первой строки 100, а каждый второй элемент последней строки сделаем равными 42.

matrix[0, :] += 100
matrix[-1, ::2] = 42
print(matrix)
[[ 95 102 103 104 105]
 [ -4   7   8   9  10]
 [ -3  12  13  14  15]
 [ -2  17  18  19  20]
 [ 42  22  42  24  42]]