
Использование списков ч.3
Срезы, де-структуризация списков. Операторы упаковки и распаковки
Штана Альберт Игоревич
Использование списков ч.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 по значку ▶.