Привет, Хабр! ??

Меня зовут Олег Булыгин, я data scientist, аналитик, автор и спикер IT-курсов.

Я готовлю разный полезный контент, туториалы и руководства по Python, которыми бы хотел делиться с вами :)

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

Все сталкиваются с индексами на самых ранних стадиях освоения языка, как правило, при изучении списков. Вероятно, вы и так знаете, что индексация в Python начинается с нуля. У нас есть список movies, тогда операция movies[0] вернёт первый элемент списка.

Да, для новичков считать от нуля до девяти при работе со списком из десяти элементов поначалу кажется немного странным. Python в этом не уникален — в большинстве языков программирования реализован такой же подход (C, C++, Java, C# и JavaScript).

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

  • Она почти никогда не используется в циклах. Да, мы можем перебирать индексы элементов объекта в цикле for вместо перебора собственно элементов, но это не норма.

  • Можно использовать отрицательные индексы, они начинаются с -1. -1 возвращает последний элемент, -2 возвращает предпоследний и так далее.

  • Для извлечения сразу нескольких элементов можно использовать расширенную форму индексации — срезы. Используя срезы в сочетании с отрицательными индексами можно, например, развернуть последовательность. Также можно указывать шаг среза для составления гибких правил извлечения нужных элементов.

Если вы новичок в Python, и вам пока не знакомы эти концепции, то в этой статье мы как раз рассмотрим несколько практических примеров.

Простая прямая индексация

Давайте начнём с нескольких простых примеров прямой индексации, используя список, кортеж и строку. Как показано ниже, индекс — это число, заключённое в квадратные скобки, которое мы ставим после составного объекта.

numbers = [42, 1941, 1066, 1969]
indexes = "Всё очень просто!"
names = ("Оруэлл", "Хаксли", "Замятин")

print(numbers[0])
# 42 

last_index = len(indexes) - 1
print(indexes[last_index])
# ! 

print(f"Нас ждёт будущее, как в книгах {names[1]}.")
# Нас ждёт будущее, как в книгах Хаксли.

Опять же, во всех случаях индекс первого элемента равен нулю, а последнего — длина объекта минус единица. Использование индекса за пределами этого диапазона приведёт к тому, что Python выдаст ошибку IndexError.

А теперь давайте обсудим нюансы индексации, которые специфичны именно для Python.

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

numbers = [1, 2, 8, 4]
print(numbers) 
# [1, 2, 8, 4] 

# Изменяем третий элемент списка
numbers[2] = 3 
print(numbers)
# [1, 2, 3, 4]

Обратная индексация в Python

Обратная индексация в Python предполагает доступ к элементам при помощи отрицательных чисел. Она начинается с конца объекта и идёт в обратном порядке. То есть, мы можем получить последний элемент при помощи индекса -1. Доступ к предпоследнему элементу можно получить с помощью -2 и так далее.

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

Давайте возьмём последний символ из строки Zen of Python, используя прямую и обратную индексацию:

saying = "Simple is better than complex" 

# получаем последний элемент прямой индексацией
print(saying[len(saying) - 1]) 
# x

# используем обратную
print(saying[-1])
# x

Работа с индексами в цикле for

Как мы упоминали выше, в общем случае индексы не используются в циклах Python хотя в некоторых языках без индексов не реализовать итерацию по элементам составного объекта. Вот, например, как это может выглядеть на C:

#include <stdio.h>

int main(void) 
{
    const int LEN = 3; 
    char chars[LEN] = {"A", "B", "C"}; 
    
    for(int i = 0; i < LEN; i++) 
    { 
        printf("Найден символ по индексу %d: %c\n", i, chars[i]); 
    } 
}
/* 
Найден символ по индексу 0: A
Найден символ по индексу 1: B
Найден символ по индексу 2: C
*/

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

chars = ["A", "B", "C"] 
for char_ in chars: 
    print(char_)

# A
# B
# C

Так всегда и нужно писать за исключением редких ситуаций, когда нам напрямую нужно оперировать с индексами в рамках логики какого-то алгоритма. Тут поможет функция enumerate, которая позволяет получить и индекс, и значение одновременно. Вот как мы можем получить тот же результат, что и в коде C:

chars = ["A", "B", "C"] 
for index, char in enumerate(chars): 
    print(f"Найден символ по индексу {index}: {char}")

# Найден символ по индексу 0: A 
# Найден символ по индексу 1: B
# Найден символ по индексу 2: C

Срезы Python: индексы на стероидах

Срезы — главное, что отличает функционал индексов в Python от многих других языков. Если индекс позволяет нам извлекать один элемент, то срезы позволяют нам извлекать (или присваивать) сразу несколько элементов. Как и в случае с индексами, выражение помещается после имени объекта в квадратные скобки и имеет следующий базовый синтаксис:

sequence[start:stop:step]

  • Значение start — это целое число, которое является началом (левой границей) среза. Если его не ставить, то по умолчанию равен нулю, то есть началу последовательности.

  • Значение stop — это целое число, представляющее собой конец среза (его правую границу). Очень важно помнить, что правая граница предполагает нужный вам последний индекс + 1. То есть правая граница сама по себе в результат не входит. Если её не ставить, то по умолчанию используется длина объекта («до самого конца»).

  • Значение step — целое число, шаг среза, по умолчанию равен 1. Шаг среза последовательно прибавляется к каждому индексу от левой границы до правой, результирующие элементы будут в выборке. Т.е. если шаг равен 1, то берётся каждый элемент, если 2 — через один. А если шаг равен -1, то элементы выбираются справа налево.

Давайте посмотрим на это наглядно, начав со срезов с прямой индексацией:

numbers = [1, 2, 3, 4, 5] 

# срезы от нуля до двух, с явным или неявным началом среза 
print("Индексы от нуля до двух") 
print(numbers[0:3]) 
print(numbers[:3]) # вариант аналогичный предыдущему
# Индексы от нуля до двух
# [1, 2, 3] [1, 2, 3] 

# Индексы от 3 до конца списка
print("\nИндексы от 3 до конца списка") 
print(numbers[3:len(numbers)]) 
print(numbers[3:]) 
# Индексы от 3 до конца списка
# [4, 5] 
# [4, 5] 

# Делаем неглубокую копию списка 
print("\nКопия списка") 
print(numbers[:]) 
# Копия списка
# [1, 2, 3, 4, 5] 

# Получем все элементы через 1
print("\nС шагом 2:") 
print(numbers[::2])
# С шагом 2:
# [1, 3, 5]

Срезы могут быть удобными, например, для удаления фиксированного префикса из строк:

# Удаляем "id-" из строк в списке: 
order_items = ["id-999", "id-19098", "id-2"] 
cleaned = [item[3:] for item in order_items] 
print(cleaned)
# ['999', '19098', '2']

В срезах, конечно, можно использовать и обратную индексацию. Если задать шаг -1, то получим элементы в обратном порядке.

numbers = [1, 2, 3, 4, 5] 
print(numbers[::-1]) 
print(numbers[4:2:-1])
# [5, 4, 3, 2, 1] 
# [5, 4]

Отрицательный шаг в срезах в реальной практике используется нечасто, но на собеседованиях вы вполне можете натолкнуться на вопрос о том, как развернуть строку. Это можно сделать и при помощи цикла, но не такого ответа в идеале от вас ожидают :)

Срезы для присваивания

Присвоение по срезу заменяет часть составного объекта содержимым другого составного объекта. Количество добавляемых элементов не обязательно должно соответствовать количеству элементов в срезе, т.к. список без проблем увеличиться или уменьшиться, если их будет больше или меньше.

Например:

count_to_ten = [num for num in range(1, 11)] # список с числами от 1 до 10
count_to_ten[3:6] = [20, 30] 
count_to_ten 
count_to_ten[6:8] = [100, 200, 300, 400] 
print(count_to_ten)
# [1, 2, 3, 20, 30, 7, 100, 200, 300, 400, 10]

Напоминаем, что для строк и других неизменяемых типов данных присваивание по индексу/срезу работать не будет.

Задачки по индексации и срезам

Решите несколько небольших задачек самостоятельно для закрепления материара:

1. Что мы получим в результате запуска приведённого ниже кода? Сможете ли вы заменить все строки, кроме импорта, на один print, который выведет аналогичную строку?

from string import ascii_uppercase 
subset = ""
for idx, letter in enumerate(ascii_uppercase): 
    if idx % 4 == 0: 
        subset = subset + letter 
print(subset)

2. Используя ascii_uppercase, выведите алфавит в обратном порядке при помощи среза.

3. Находим иголку в стоге сена. При помощи срезов, метода index и функции len выведите строку "иголка", где бы она ни располагалась в example.

example = "сено сено сено иголка сено сено сено, привет, привет, пока."

4. При помощи среза из приведённого ниже списка выведите такой результат:[9, 6, 3]

count_to_ten = [num for num in range(1,11)] 
print(count_to_ten)

5. Из имеющегося списка при помощи индексов выведите на экран только слово "клубнику".

tokens = "Тут хоть где-нибудь можно купить клубнику?".split(" ") 
print(tokens)

6. Как думаете, что мы увидим в результате вызова claim.index("Python")?

claim = "В том материалы вы узнали про индексы и срезы в Python".split() 
print(claim)

7. Что увидим на экране в качестве вывода?

greeting = "Hello" 
print(greeting[4]) 
print(greeting[5])

?Если тебе интересны и другие полезные материалы по Python и IT, то подписывайся на мой канал в tg: PythonTalk ?

Комментарии (5)


  1. rsashka
    29.04.2024 06:41

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

    Странным для не новичка является путать номер элемента с его индексом (смещением элемента относительно начала массива).


  1. rSedoy
    29.04.2024 06:41
    +1

    for char_ in chars:
    а что стало причиной появление подчеркивания?


    1. Yuri0128
      29.04.2024 06:41

      Ну ХЗ что автор хотел, делая такую запись. Но, в принципе, - вполне себе нормальная практика именовать, заменяя "s" в именах на "_" при обходе списков (ну и др. п.в.). Вполне понятно что и к чему относится. Ну, на мой взгляд.

      Вот почему в других местах не так у автора - вот это уже вопрос....


  1. Yuri0128
    29.04.2024 06:41

     без индексов цикл просто не написать. Вот, например, как это может выглядеть на C:

    Ну... Во-первых i в данном случае - это счетчик цикла и аж никак не индекс чего-то там.. То, что вы его используете в качестве индекса, в целом его назначение не меняет.

    Во-вторых - ну используйте цикл while() - там нету счетчика цикла.

    Ну и даже цикл со счетчиком в С-ях будет отрабатываться сильно быстрее кода на Пайтоне...

    PS Вы бы еще Пайтон с ассебмлером сравнили - вот там без индексной адресации сильно неудобнее реализация будет. А с индексной - шустро выходит.


  1. SatCat
    29.04.2024 06:41

    Автор не рассказал о работе со срезом в виде конструкции my_list[0:0] = [1,2,3]