Срезы строк в Python: как работает slicing с примерами

Первый раз я столкнулся со срезами, когда пытался вытащить доменное имя из email-адреса в лог-файле. Нужно было взять подстроку между символом @ и пробелом. Два индекса, двоеточие, и Python вернул ровно то, что нужно. Срез (slice) это способ получить часть последовательности, указав начальную позицию, конечную позицию и шаг. Синтаксис выглядит так: sequence[start:stop:step].
Эта статья разбирает механику срезов строк от базового синтаксиса до отрицательных шагов, объясняет типичные ошибки и показывает, как срезы работают со списками и пользовательскими объектами.
Индексы строк: фундамент для срезов
Прежде чем разбирать срезы, нужно разобраться с индексами. Строка в Python это последовательность символов, каждый из которых имеет числовой индекс. Первый символ получает индекс 0, второй 1 и так далее.
fruit = "banana"
print(fruit[0]) # b
print(fruit[1]) # a
print(fruit[5]) # a
Длина строки "banana" равна 6 (символы с индексами от 0 до 5). Попытка обратиться к fruit[6] вызовет IndexError.
Отрицательные индексы
Python поддерживает отрицательную индексацию. Индекс -1 указывает на последний символ, -2 на предпоследний.
fruit = "banana"
print(fruit[-1]) # a (последний символ)
print(fruit[-2]) # n
print(fruit[-6]) # b (первый символ)
Отрицательные и положительные индексы ссылаются на те же символы. Для строки "banana" fruit[0] и fruit[-6] дают одинаковый результат "b".
b a n a n a
0 1 2 3 4 5 ← положительные
-6 -5 -4 -3 -2 -1 ← отрицательные
Базовый синтаксис среза: start и stop
Срез записывается в квадратных скобках через двоеточие: s[start:stop]. Python возвращает подстроку от индекса start (включительно) до индекса stop (не включая).
s = "Monty Python"
print(s[0:5]) # Monty
print(s[6:12]) # Python
Индекс stop не входит в результат. Это правило звучит непривычно, но даёт удобное свойство: длина среза равна stop - start.
s = "banana"
print(s[0:3]) # ban (3 символа: 0, 1, 2)
print(s[3:6]) # ana (3 символа: 3, 4, 5)
Пропуск start и stop
Если start не указан, Python начинает с начала строки. Если stop не указан, срез идёт до конца.
fruit = "banana"
print(fruit[:3]) # ban (от начала до индекса 3)
print(fruit[3:]) # ana (от индекса 3 до конца)
print(fruit[:]) # banana (копия всей строки)
Срез fruit[:] создаёт копию строки. Для неизменяемых строк это не имеет практического значения, но для списков list[:] часто используется для создания поверхностной копии.
Срез с одинаковыми start и stop
fruit = "banana"
print(fruit[3:3]) # пустая строка
Когда start равен stop, результат всегда пустая строка. Никакой ошибки Python не выбрасывает.
Третий параметр: шаг (step)
Полный синтаксис среза: s[start:stop:step]. Параметр step определяет, через сколько символов брать следующий.
Шаг больше единицы
s = "abcdefghij"
print(s[0:10:2]) # acegi (каждый второй символ)
print(s[1:10:2]) # bdfhj (каждый второй, начиная с индекса 1)
print(s[::3]) # adgj (каждый третий символ)
При step=2 Python берёт символы с индексами 0, 2, 4, 6, 8. При step=3 с индексами 0, 3, 6, 9.
Отрицательный шаг
Отрицательный шаг переворачивает направление обхода. Python идёт от большего индекса к меньшему.
s = "abcdef"
print(s[::-1]) # fedcba (реверс строки)
print(s[::-2]) # fdb (каждый второй в обратном порядке)
Запись s[::-1] это каноничный способ развернуть строку в Python. Без start и stop Python берёт всю строку и обходит её справа налево.
Отрицательный шаг с границами
При отрицательном шаге start должен быть больше stop. Иначе срез вернёт пустую строку.
s = "abcdef"
print(s[4:1:-1]) # edc (от индекса 4 до индекса 2, шаг -1)
print(s[1:4:-1]) # пустая строка (start < stop при шаге -1)
Границы при отрицательном шаге: start указывает, откуда начинаем (включительно), stop указывает, где останавливаемся (не включая). Направление обхода определяется знаком step.
s = "информатика"
print(s[-1::-1]) # акитамрофни (от последнего до первого)
print(s[-2:1:-1]) # китамрофн (от предпоследнего до индекса 2)
Срезы не вызывают IndexError
Одно из удобных свойств срезов: выход за границы строки не вызывает ошибку. Python просто обрезает результат до доступных символов.
s = "hello"
print(s[0:100]) # hello (stop выходит за границу, но ошибки нет)
print(s[-100:3]) # hel (start выходит за границу)
print(s[10:20]) # пустая строка (оба за границей)
Это отличается от обычного индексирования: s[100] вызовет IndexError, а s[0:100] вернёт всю строку.
Как Python вычисляет срез внутри
Когда вы пишете s[start:stop:step], Python создаёт объект slice(start, stop, step) и передаёт его методу __getitem__. Метод slice.indices(length) вычисляет реальные значения start, stop, step с учётом длины последовательности.
s = slice(1, 10, 2)
print(s.indices(6)) # (1, 6, 2)
Функция indices() приводит значения к корректному диапазону. Здесь stop=10 было урезано до 6 (длина строки). Этот механизм объясняет, почему срезы не вызывают IndexError: границы нормализуются до реальных индексов.
Именованные срезы
Объект slice() можно сохранить в переменную и применять многократно.
YEAR = slice(0, 4)
MONTH = slice(5, 7)
DAY = slice(8, 10)
date_str = "2026-04-07"
print(date_str[YEAR]) # 2026
print(date_str[MONTH]) # 04
print(date_str[DAY]) # 07
Именованные срезы делают код читаемее, когда одна и та же структура разбирается в нескольких местах программы.
Практические примеры со строками
Извлечение домена из email
data = "From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008"
at_pos = data.find("@")
sp_pos = data.find(" ", at_pos)
host = data[at_pos + 1 : sp_pos]
print(host) # uct.ac.za
Метод find() находит позицию символа, а срез извлекает подстроку между двумя позициями.
Проверка палиндрома
def is_palindrome(s):
cleaned = s.lower().replace(" ", "")
return cleaned == cleaned[::-1]
print(is_palindrome("А роза упала на лапу Азора")) # True
print(is_palindrome("hello")) # False
Срез [::-1] разворачивает строку. Если строка равна своему реверсу, она палиндром.
Маскирование номера карты
card = "4111111111111111"
masked = card[:4] + " **** **** " + card[-4:]
print(masked) # 4111 **** **** 1111
Срез card[:4] берёт первые четыре цифры, card[-4:] последние четыре.
Удаление расширения файла
filename = "report_2026.pdf"
dot_pos = filename.rfind(".")
name = filename[:dot_pos]
print(name) # report_2026
Метод rfind() ищет последнюю точку (на случай, если в имени файла несколько точек), а срез берёт всё до неё.
Извлечение каждого второго символа
cipher = "Phyetlhloonг"
print(cipher[::2]) # Python
print(cipher[1::2]) # hellо г
Срезы с шагом 2 позволяют разделить чётные и нечётные позиции строки.
Срезы списков
Срезы работают одинаково для любых последовательностей: строк, списков, кортежей. Но у списков есть дополнительная возможность: присваивание срезу.
Чтение среза списка
numbers = [10, 20, 30, 40, 50, 60]
print(numbers[1:4]) # [20, 30, 40]
print(numbers[::2]) # [10, 30, 50]
print(numbers[::-1]) # [60, 50, 40, 30, 20, 10]
Срез списка возвращает новый список. Оригинал не меняется.
Присваивание срезу
a = [1, 2, 3, 4, 5]
a[2:4] = [7, 8, 9] # заменяем 2 элемента на 3
print(a) # [1, 2, 7, 8, 9, 5]
b = [10, 20, 30, 40, 50]
b[1:4] = [] # удаляем элементы с 1 по 3
print(b) # [10, 50]
c = [1, 2, 5]
c[2:2] = [3, 4] # вставка без удаления
print(c) # [1, 2, 3, 4, 5]
При присваивании непрерывному срезу (без шага) количество новых элементов может отличаться от количества заменяемых. Список автоматически вырастет или сократится.
Присваивание срезу с шагом
a = [1, 2, 3, 4, 5, 6, 7]
a[::2] = [10, 20, 30, 40] # a[::2] содержит 4 элемента
print(a) # [10, 2, 20, 4, 30, 6, 40]
При присваивании срезу с шагом количество новых элементов обязано совпадать с количеством позиций в срезе. Иначе Python выбросит ValueError.
Копирование списка через срез
original = [1, 2, 3]
copy = original[:]
copy.append(4)
print(original) # [1, 2, 3] (не изменился)
print(copy) # [1, 2, 3, 4]
Срез [:] создаёт поверхностную копию (shallow copy). Для вложенных списков элементы копируются по ссылке, а не по значению.
Срезы и строковая неизменяемость
Строки в Python неизменяемы (immutable). Присвоить значение срезу строки нельзя.
greeting = "Hello, world!"
try:
greeting[0:5] = "Jello"
except TypeError as e:
print(e)
# 'str' object does not support item assignment
Для изменения части строки нужно создать новую:
greeting = "Hello, world!"
new_greeting = "Jello" + greeting[5:]
print(new_greeting) # Jello, world!
Сравнение индексирования и среза
Эта разница объясняет, почему s[100] падает с ошибкой, а s[0:100] спокойно возвращает строку.
Реализация срезов в своих классах
Чтобы поддержать срезы в пользовательском классе, нужно реализовать метод __getitem__. Когда к объекту применяют [] с двоеточием, Python передаёт в __getitem__ объект slice.
class MyList:
def __init__(self, data):
self._data = list(data)
def __getitem__(self, key):
if isinstance(key, slice):
return MyList(self._data[key])
return self._data[key]
def __repr__(self):
return f"MyList({self._data})"
m = MyList([10, 20, 30, 40, 50])
print(m[1:4]) # MyList([20, 30, 40])
print(m[::2]) # MyList([10, 30, 50])
print(m[3]) # 40
Неочевидные детали
Первый факт: шаг не может быть нулём. Запись s[::0] вызовет ValueError: slice step cannot be zero.
Второй факт: срез строки возвращает строку, срез списка возвращает список, срез кортежа возвращает кортеж. Тип результата всегда совпадает с типом исходного объекта.
Третий факт: s[:n] + s[n:] всегда равно s для любого n, даже если n выходит за границы. Это свойство полезно при разбиении строки на две части.
s = "Python"
for n in range(-10, 10):
assert s[:n] + s[n:] == s # никогда не упадёт
Четвёртый факт: в CPython если срез совпадает со всей строкой (s[:]), Python может вернуть ссылку на тот же объект, потому что строки неизменяемы.
Пятый факт: для байтовых строк (bytes) и bytearray срезы работают по тому же принципу. Срез bytes возвращает bytes, срез bytearray возвращает bytearray.
FAQ
Почему stop не включается в результат?
Это соглашение Python, заимствованное из C. Полуоткрытый интервал [start, stop) даёт два полезных свойства: len(s[a:b]) == b - a и s[:n] + s[n:] == s. Оба перестали бы работать, если бы stop включался.
Можно ли использовать переменные в качестве границ среза?
Да. Границы это обычные целые числа или None. Переменные, выражения, вызовы функций допустимы: s[start:end], s[:len(s)//2], s[a+1:b-1].
Как получить каждый n-й символ начиная с конца?
Используйте отрицательный шаг: s[::-n]. Например, s[::-3] вернёт каждый третий символ в обратном порядке.
Как развернуть слова в предложении, сохранив порядок букв?
Разделите строку, разверните список, соедините обратно:
sentence = "Python очень мощный"
reversed_words = " ".join(sentence.split()[::-1])
print(reversed_words) # мощный очень Python
Здесь split() создаёт список слов, [::-1] разворачивает его, join() склеивает обратно.
Срезы работают с range?
Да, начиная с Python 3. range поддерживает и индексирование, и срезы, которые возвращают новый range-объект:
r = range(0, 20, 2)
print(r[2:5]) # range(4, 10, 2)
print(list(r[2:5])) # [4, 6, 8]
Мой совет: запомните три правила и срезы перестанут путать. Первое: start включается, stop нет. Второе: пропущенные границы означают "от начала" и "до конца". Третье: отрицательный шаг переворачивает направление, и тогда start должен быть больше stop.
