Все статьи по Python
26 апр. 2025 г. - 19 мин. чтения
Использование списков ч.3

Использование списков ч.3

Срезы, де-структуризация списков. Операторы упаковки и распаковки

@ashtana

Штана Альберт Игоревич

Использование списков ч.3

Срезы

Настало время перейти к очень интересному инструменту, который Python предоставляет для работы с целыми подмножествами элементов списка - срезы(slices).

Синтаксис срезов

Срез записывается так же, как записывается обращение к элементу списка по индексу:

some_list[START:STOP:STEP]

У среза три параметра:

  • START — индекс первого элемента в выборке
  • STOP — индекс элемента списка, перед которым срез должен закончиться. Сам элемент с индексом STOP не будет входить в выборку
  • STEP — шаг выбираемых индексов
l = [1, 2, 3, 4, 5]
# Срез от нулевого по четвёртый индекс с шагом 1
slice = l[0:4:1]
print(slice)  # => [1, 2, 3, 4]
# Срез от нулевого по четвёртый индекс с шагом 2
slice = l[0:4:2]
print(slice)  # => [1, 3]

При этом любой из трех параметров среза может быть пропущен и вместо соответствующего параметра будет значение по умолчанию:

  • По умолчанию START означает «от начала списка».
  • По умолчанию STOP означает «до конца списка включительно».
  • По умолчанию STEP означает «брать каждый элемент».

Вот несколько примеров с разными наборами параметров:

  • [:] или [::] — весь список.
  • [::2] — нечетные по порядку элементы.
  • [1::2] — четные по порядку элементы.
  • [::-1] — все элементы в обратном порядке.
  • [5:] — все элементы, начиная с шестого.
  • [:5] — все элементы, не доходя до шестого.
  • [-2:1:-1] — все элементы от предпоследнего до третьего в обратном порядке. Во всех случаях выборки от большего индекса к меньшему нужно указывать шаг. Теперь разберем как можно использовать срезы.
Выборка элементов

Срезы работают не только со списками, но и с кортежами, и даже со строками. Результатом применения выборки всегда становится новое значение соответствующего типа — список, кортеж, строка:

word = "hello"
print(word[3:])  # => lo

user_data = (
    "Alex",
    "alex@example.com",
    "student",
)
print(user_data[1::])  # ('alexn@example.com', 'student')

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print(numbers[1::2])  # [2, 4, 6, 8, 10]
Присваивание срезу

В отличие от строк и кортежей списки могут изменяться. Одним из вариантов модификации является присваивание срезу. Срезу с указанным шагом можно присвоить список из новых элементов:

l = [1, 2, 3, 4, 5, 6]
l[::2] = [0, 0, 0]
print(l)  # => [0, 2, 0, 4, 0, 6]

Срез [::2] означает, что мы выбираем элементы с шагом 2, то есть берем каждый второй элемент. Список [0, 0, 0] - это список, который мы присваиваем выбранным элементам. Срез l[::2] теперь содержит элементы с индексами 0, 2 и 4 (т.е. 1, 3 и 5). Присваивание [0, 0, 0] этим элементам заменяет их на нули. В результате изменения списка l становится равным [0, 2, 0, 4, 0, 6].

Если вы попробуете присвоить срезу с шагом неверное количество элементов, то получите ошибку:

l = [1, 2, 3, 4]
l[::2] = [5, 6, 7]
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# ValueError: attempt to assign sequence of size 3 to extended slice of size 2

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

l = [1, 2, 3]
l[2:] = [4, 5]
print(l)  # => [1, 2, 4, 5]
l[1:-1] = [100]
print(l)  # => [1, 100, 5]
l[:] = []
print(l)  # => []

Сначала список растет, потом уменьшается, а под конец вообще становится пустым — и все с помощью компактного, но мощного синтаксиса срезов.

Срезы-значения

Хоть срезы и имеют специальную поддержку со стороны синтаксиса, но можно создавать и использовать срезы сами по себе — как обычные значения.

Значение среза можно сконструировать с помощью функции slice():

first_two = slice(2)
each_odd = slice(None, None, 2)
print(each_odd)  # => slice(None, None, 2)
l = [1, 2, 3, 4, 5]
print(l[first_two])  # => [1, 2]
print(l[each_odd])  # => [1, 3, 5]

Функция slice() принимает от одного до трех параметров — те самые START, STOP и STEP. При вызове функции с одним параметром, функция вызывается с параметром STOP. Если необходимо пропустить один из параметров, то подставляется вместо него None. Также None можно использовать и в записи срезов в квадратных скобках — там он так же будет означать пропуск значения. На месте параметров среза могут быть любые выражения, лишь бы эти выражения вычислялись в целые числа или None.

Соотношение start и stop

В срезе элемент с индексом STOP не включается в результат, в отличие от элемента с индексом START.

Эту особенность можно использовать, какой бы неотрицательный индекс n мы ни выбрали, для любого списка будет соблюдаться указанное равенство:

l = [1, 2, 3, 4, 5]
n = 2
l == l[:n] + l[n:]  # True

Ещё пример:

word = "Hello!"
print(word[:2] + word[2:])  # => 'Hello!'
print(word[:4] + word[4:])  # => 'Hello!'
print(word[:0] + word[0:] == word)  # => True
print(word[:100] + word[100:] == word)  # => True

Destructuring

Destructuring – синтаксическая возможность "раскладывать" элементы списка (и не только) в отдельные переменные. Деструктуризация относится к необязательным, но очень приятным возможностям языка. Представьте, что у нас есть список из двух элементов, которыми мы хотим оперировать в нашей программе. Самый простой вариант использования его элементов — постоянное обращение по индексу point[0] и point[1].

point = [1, 1]
print(f`{point[0]}:{point[1]}`) # => 1:2

Индексы ничего не говорят о содержимом, и для понимания этого кода придется прикладывать дополнительные усилия. Гораздо лучше сначала присвоить эти значения переменным с хорошими именами. Тогда код станет читаемым:

x = point[0]
y = point[1]
print(f`{x}:{y}`) # => 1:2

Код стал значительно понятнее, хотя и длиннее. С помощью деструктуризации то же самое можно сделать короче:

[x, y] = point
# Слева список повторяет структуру правого списка
# но вместо значений используются идентификаторы
# они заполняются значениями, стоящими на тех же позициях в правом списке
# [x, y] = [1, 2]
# x = 1, y = 2
print(f`{x}:{y}`) # => 1:2

Деструктуризация работает на любом уровне вложенности. Например, с ее помощью можно извлекать данные из списков внутри списков:

[one, [two, three]] = [1, [2, 3]]
print(one, two, three)  # => 1 2 3

Деструктуризация в циклах

Разложение списка можно использовать не только как отдельную инструкцию в коде, но и в циклах:

points = [[1, 2], [0, -1]]
for x, y in points:
    print([x, y])
# => [1, 2]
# => [0, -1]

В этом примере каждый элемент в цикле является списком. Без деструктуризации цикл выглядит так:

for item in points:
    print(item)

Внутри for переменная item - это список, поэтому вместо нее можно подставить деструктуризацию [x, y].

Деструктуризация строк

В python строки ведут себя подобно спискам и их также можно деструктурировать.

[first, second, third] = "two"
print(first)  # => 't'

Оператор упаковки

Мощь деструктуризации больше всего проявляется там, где она используется вместе с синтаксисом упаковки-распаковки. Оператор * (в Python у него нет фиксированного названия, но часто используют "оператор распаковки/упаковки аргументов") позволяет "свернуть" часть элементов во время деструктуризации. Например, с его помощью можно разложить список на первый элемент и все остальные:

fruits = ["apple", "orange", "banana", "pineapple"]
# Разделяем первый элемент и оставшиеся элементы
first, *rest = fruits
print(first)  # => 'apple'
print(rest)  # => ['orange', 'banana', 'pineapple']

Запись *rest означает, что нужно взять все элементы, которые остались от деструктуризации и поместить их в список с именем rest. Этому списку можно дать любое имя. Упаковка срабатывает в самом конце, когда все остальные данные уже разложены по своим переменным. Именно поэтому мы и назвали список rest, оставшиеся. Подобным образом любой список раскладывается на любое количество элементов + остальные.

# Исходный список
fruits = ["apple", "orange", "banana", "pineapple"]
head, *tail = fruits
print(head)  # => 'apple'
print(tail)  # => ['orange', 'banana', 'pineapple']
# Первый, второй и оставшиеся элементы
first, second, *rest = fruits
print(first)  # => 'apple'
print(second)  # => 'orange'
print(rest)  # => ['banana', 'pineapple']
# Если элементов нет, то rest окажется пустым списком
first, second, third, one_more, *rest = fruits
print(first)  # => 'apple'
print(second)  # => 'orange'
print(third)  # => 'banana'
print(one_more)  # => 'pineapple'
print(rest)  # => []
# Пропуск элемента
first, _, third, *rest = fruits
print(first)  # => 'apple'
print(third)  # => 'banana'
print(rest)  # => ['pineapple']
# Также можно упаковывать элементы в любом месте списка
# Первый, последний и оставшиеся центральные элементы
first, *mid, last = fruits
print(first)  # => 'apple'
print(last)  # => 'pineapple'
print(mid)  # => ['orange', 'banana']

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

# Исходный список
fruits = ["apple", "orange", "banana", "pineapple"]
# Срез списка, начиная с элемента с индексом 1
rest = fruits[1:]
print(rest)  # ['orange', 'banana', 'pineapple']

Синтаксис упаковки можно применять также и при деструктуризации строк.

string = "some string"
first, second, *rest = string
print(first)  # => 's'
print(second)  # => 'o'
print(rest)  # => ['m', 'e', ' ', 's', 't', 'r', 'i', 'n', 'g']

Обратите внимание, что после упаковки оставшейся части строки в rest мы получаем список, а не строку. Деструктуризация в Python позволяет эффективно разделять элементы коллекций на отдельные переменные и собирать оставшиеся элементы в список с помощью синтаксиса оператора упаковки аргументов - *. Это удобно для работы с данными, когда нужно отделить часть элементов от остальных. Синтаксис * можно использовать для разделения коллекций на фиксированное количество элементов и оставшиеся.

Оператор распаковки

У оператора упаковки есть вторая функциональность - распаковка. Она имеет такой же синтаксис, но выполняет противоположную задачу: не сворачивает элементы, а наоборот, растягивает их. С ее помощью обычно копируют или соединяют списки. Представьте, что нам нужно определить список, добавив туда элементы из другого списка. Такая задача часто встречается при работе со значениями по умолчанию:

# Исходные списки
french_cities = ["paris", "marseille"]
cities = ["milan", "rome", *french_cities]
print(cities)  # => ['milan', 'rome', 'paris', 'marseille']

В этом случае * — это распаковка. Оператор растянул список, добавив все его элементы в новый список. Как понять какая функциональность используется? Все дело в контексте использования. Если * появляется слева от знака равно, то происходит упаковка в переменные. Если * стоит справа от знака равно, то происходит упаковка в список.

# Исходные списки
french_cities = ["paris", "marseille"]
cities = [*french_cities, "milan", "rome"]
print(cities)  # => ['paris', 'marseille', 'milan', 'rome']
french_cities = ["paris", "marseille"]
cities = ["milan", *french_cities, "rome"]
print(cities)  # => ['milan', 'paris', 'marseille', 'rome']

* работает с любым количеством списков:

# Исходные списки
french_cities = ["paris", "marseille"]
italian_cities = ["rome", "milan"]
# Объединение списков с использованием *
cities = [*french_cities, *italian_cities]
print(cities)  # ['paris', 'marseille', 'rome', 'milan']

Попробуйте сами запустить код в окне ниже с интерпретатором Python и повторите примеры из статьи чтобы самим увидеть и понять как всё это работает. Для этого в ячейке с кодом нажмите клавиши на клавиатуре Shift+Enter или запустите код через кнопку Run по значку ▶.