PyWay – гуру Python 🐉
825 subscribers
22 photos
1 video
2 files
153 links
Ваш путь в глубины Python. Узнай все секреты и фишки у нас! Если хотите сотрудничать – пишите @account1242 (условия @pyway_ads)
Download Telegram
Срезы (или слайсы – от. slice) позволяют получить гибкий доступ к частям списков, кортежей, строк и других последовательных типов. Причем, как на чтение, так и на запись и удаление. Ниже я приведу несколько примеров использования срезов.

😋 Помните! Индексирование в Python идет с 0. Первый элемент имеет индекс 0.

Общая нотация среза выглядит так: arr[start:stop:step], где start, stop и step могут быть как числами, так и переменными или выражениями, но кроме того могут быть и вовсе пустым местом. start – это индекс элемента, с которого начинается выборка включительно, stop – индекс, на котором выборка останавливается (не включительно), step – это шаг, по умолчанию он равен 1. Все эти параметры могут быть и отрицательными, что придает им особый смысл.

🤭 Обращение по срезу не бросает исключений при выходе за границы исходной коллекции! Просто полученное множество может оказаться пустым.

Пусть есть исходный список:
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Все элементы, начиная с индекса 1:
arr[1:] == [2, 3, 4, 5, 6, 7, 8, 9, 10]
arr[5:] == [6, 7, 8, 9, 10] # или с 5 индекса
arr[100:] == [] # за пределами просто пустое множество, нет ошибки

Отрицательные значения заставляют отсчитывать элементы с конца коллекции.

Четыре последних элемента:
arr[-4:] == [7, 8, 9, 10]

ℹ️ Если вам нужен конкретный элемент, считая с конца:
arr[-1] == 10 # последний
arr[-2] == 9 # предпоследний
arr[-10] == 1 # первый – самый дальний с конца
arr[-100] # IndexError! выскочили за пределы

Вернемся к срезам. Получить первые три элемента:
arr[:3] == [1, 2, 3]

Первые элементы, не считая 6 последних:
arr[:-6] == [1, 2, 3, 4]

Диапазон. Тут индекс 4 включен (элемент равный 5), в индекс, равный 7 (это элемент со значением 8 ) уже нет.
arr[4:7] == [5, 6, 7]
arr[100:200] == [] # за пределами просто пустое множество, нет ошибки

Диапазон с 5-го элемента с конца до 2-го элемента с конца.
arr[-5:-2] == [6, 7, 8]
arr[-2:-5] == [] # порядок важен, потому что 2-ой с конца идет позже 5-го с конца!

С 8-го с конца до 4-го с начала:
arr[-8:4] == [3, 4]

С индекса 2 с начала до 3-го элемента с конца:
arr[2:-3] == [3, 4, 5, 6, 7]

Работа с шагом.

Каждый второй элемент, иными словами индексы 0, 2, 4 и так далее:
arr[::2] == [1, 3, 5, 7, 9]

Каждый третий элемент:
arr[::3] == [1, 4, 7, 10]

Каждый второй элемент, со сдвигом в 1 (индексы 1, 3, 5, ...):
arr[1::2] == [2, 4, 6, 8, 10]

Элементы c индекса 1 до 8 с шагом 4:
arr[1:8:4] == [2, 6]

Шаг не может быть нулем, иначе какой же это шаг? Это стояние на месте:
arr[::0] # ValueError

Отрицательный шаг всегда разворачивает подмножество. Список задом наперед:
arr[::-1] == [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

Каждый второй элемент с конца (индексы 9, 7, 5 и так далее):
arr[::-2] == [10, 8, 6, 4, 2]

С отрицательным шагом начальный индекс должен быть позже конечного:
arr[-2:-5:-1] == [9, 8, 7]
arr[5:2:-1] == [6, 5, 4]

Не выйдет досчитать с 2 до 5 в отрицательную сторону. После индекса 2 следует 1, таким образом, до 5 мы никогда не дойдем.
arr[2:5:-1] == []
arr[-5:-2:-1] == []


⚠️ Пропуски между двоеточиями подразумеваются как None, а не как 0:
arr[::-2] == arr[None:None:-2] == [10, 8, 6, 4, 2]
arr[0::-2] == [1] # читай: начни с 0 индекса и шагай до упора влево по 2.

Условие start=None можно читать как "с самого края", а stop=None можно читать как "до упора".
С какого до какого края – это зависит от знака step, как вы понимаете.

Подобным образом можно работать и со строками, вырезая из них подстроки.
"hello world"[1:-3] == 'ello wo'
"hello world"[-2::-1] == 'lrow olleh'

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

В следующем посте расскажу про запись и удалением по срезам.
✂️ Работа со срезами на запись и удаление

Во-первых, самое главное. При присвоении по срезу к другой переменной создается поверхностная копия данных!
Поверхностная (shallow) копия – это копирование ссылок на нужные элементы, при этом содержимое элементов не копируется.

arr = [1, 2, 3, 4, 5, 6, 7, 8]
bar = arr[1:6]
bar[3] = 8888
print(bar) # [2, 3, 4, 8888, 6] - новый изменен
print(arr) # [1, 2, 3, 4, 5, 6, 7, 8] – старый без изменений!

Лайфхак. Копирование списка: arr_copy = arr[:]

⚠️ Но! При манипуляциях над самой коллекцией – копии не создается, данные меняются в исходном объекте! Поэтому перед каждым примером подразумеваем, что arr заново равен исходному [1, 2, 3, 4, 5, 6, 7, 8].

Замена 2-го и 3-го элементов на другие:
arr[2:4] = [777, 555]
print(arr) # [1, 2, 777, 555, 5, 6, 7, 8]

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

Заменим последние два элемента на 4 новые:
arr[-2:] = [11, 12, 13, 14]
# arr == [1, 2, 3, 4, 5, 6, 11, 12, 13, 14]

Вставим в середину, а именно с 4-го индекса несколько новых чисел:
arr[4:4] = [111, 222, 333]
# arr == [1, 2, 3, 4, 111, 222, 333, 5, 6, 7, 8]

Присвоить пустой список равносильно вырезанию. Вырежем первые 5 элементов:
arr[:5] = []
# arr == [6, 7, 8]

Кстати, тот же эффект достигается выражением:
del arr[:5]

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

Заменить каждый второй элемент:
arr[::2] = [10, 20, 30, 40]
# arr = [10, 2, 20, 4, 30, 6, 40, 8]

arr[::2] = [10, 20, 30] # ValueError (не хватает чисел в правой части)
arr[::2] = [10, 20, 30, 40, 50] # ValueError (слишком много)

Можно также и с конца:
arr[::-2] = [10, 20, 30, 40]
# arr == [1, 40, 3, 30, 5, 20, 7, 10]

С правой стороны присваивания необязательно должен быть список. Может быть и генератор.
Например, обратим каждый третий элемент списка генератором:
arr[::3] = map(lambda x: 1 / x, arr[::3])
# arr == [1.0, 2, 3, 0.25, 5, 6, 0.14285714285714285, 8]

Удаление элементов с 1 по 7 (не включая 7), через 2:
del arr[1:7:2]
arr == [1, 3, 5, 7, 8]

Так как присвоение и удаление по срезу изменяют содержимое объекта, то такие операции не применимы к иммутабельным типам данных, таким как кортежи, строки, байты. Обычно их цели: списки и bytearray.
🤓 Задачка на смекалку

itertools.repeat – бесконечный генератор повторяющихся значений.

Что будет при выполнении этого кода?
Пояснение про копирование 🤏🏻

В прошлом посте вот этот пример вызвал вопросы:

arr = [1, 2, 3, 4, 5, 6, 7, 8]
bar = arr[1:6]
bar[3] = 8888
print(bar) # [2, 3, 4, 8888, 6] - новый изменен
print(arr) # [1, 2, 3, 4, 5, 6, 7, 8] – старый без изменений!


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

list1 = [333, 444]
list2 = list1[:] # копия
id(list1[0]) == id(list2[0]) # True


Оба списка имеют один и тот же объект (по тому же самому адресу) в своем составе!
Просто числа – объекты неизменяемые, иными словами, когда вы присваиваете число, то не меняете содержимое объекта по текущему адресу, вы привязываете переменную к новому объекту в другом месте памяти. А равные числа выглядят одинаково, независимо от того, в каком месте памяти они лежат 🙄

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

list1 = [ [], [] ]
list2 = list1[:]
list1[0].append('surprise')
print(list2[0]) # ['surprise']


Таким образом, поверхностная копия копирует только: количество элементов и их порядок, а сами элементы остаются ссылками на старые объекты. То есть если вы начнете добавлять, удалять, менять местами элементы list1, то порядок в list2 не пострадает. Но если вы сможете изменить внутреннее содержимое элементов списка list1, которые остались в list2, тогда оно будет затронуто в обоих местах.

Для полной глубокой копии используют библиотечную функцию deepcopy, которая рекурсивно копирует объекты:

from copy import deepcopy
list1 = [ [], [] ]
list2 = deepcopy(list1) #
👈
list1[0].append('surprise')
print(f"{list1 = }, {list2 = }")
# list1 = [['surprise'], []], list2 = [[], []] - изменение не отразилось
id(list1[0]) == id(list2[0]) # False – разные объекты!


Надеюсь, что объяснил. Если что, спрашивайте еще.
Вот так новость! Python выходит на первое место в известном рейтинге языков программирования TIOBE! 🐍
🎁 takewhile: бери, пока дают

В модуле itertools есть функция takewhile. Она позволяет выбирать из последовательности значения до тех пор, пока условие возвращает True.

У нее два параметра: predicate – предикат, или иными словами проверяющая функция и iterable – последовательность – это может быть список, кортеж, строка, итератор и т.п.

Код функции выглядит достаточно просто, примерно так:

def takewhile(predicate, iterable):
for x in iterable:
if predicate(x):
yield x
else:
break

Как только predicate вернет эквивалент False на очередном элементе, то цикл остановится.
Функция возвращает не список, а генератор. Если нужен список, то оборачиваем вызов в функцию list.

Давайте по примерам. Выбрать из списка все первые элементы меньше 10:

from itertools import takewhile
numbers = [1, 2, 5, 9, 11, 42, 3]
numbers10 = list(takewhile(lambda x: x < 10, numbers))
print(numbers10) # [1, 2, 5, 9]


Заметьте, что 3 не попала в результат, потому что встретив число 11 takewhile остановила работу. Это отличает ее от filter, которая всегда пройдет до конца.

Пример 2. Количество пробелов в начале строки:

string = " Wat? "
print(sum(1 for _ in takewhile(lambda x: x == ' ', string))) # 7
У takewhile в модуле itertools есть брат близнец из параллельной вселенной. Функция dropwhile, как можно понять из названия, будет пропускать элементы коллекции, пока выполняется условия. Ее аргументы такие же, как и у рассмотренной выше takewhile: функция проверки каждого элемента и собственно итерируемый объект.

Пока условие выполняется, функция dropwhile шагает далее. Как только впервые условие не выполнится для очередного элемента, функция начнет отдавать из себя элементы (включая этот) по очереди до конца.
dropwhile – это также генератор, и если нужен список, то обернем ее вызов в list.

Давайте посмотрим пример. Допустим дана последовательность чисел и нужно отбросить все отрицательные числа с начала:

from itertools import dropwhile

numbers = [-5, -4, -1, 0, 10, -10, 22]
numbers1 = list(dropwhile(lambda x: x < 0, numbers))
print(numbers1) # [0, 10, -10, 22]

Число -10 осталась, потому что после встречи с первым неотрицательным числом, функция отдаст его и абсолютно все последующие элементы (в отличие от filter).

Примечательно, что для любой функции и любой коллекции на входе выполняется условие:
numbers == list(takewhile(func, numbers)) + list(dropwhile(func, numbers)) # True
(Это если, конечно, не возникло ошибок)
Какой результат будет при преобразовании строки "False" к булеву типу?
Ответ:
Anonymous Quiz
29%
False
63%
True
8%
Ошибка