1. Лекция 4

12 октября 2018 г.

Заметили ошибку или есть предложение? Напишите на почту: romansdidnotcrucify@gmail.com

В данном конспекте уделите особое внимание описанию внутреннего устройства цикла `for`, а также тонкостям взятия срезов - это сэкономит вам много времени в дальнейшем.

2. Важная вещь про ejudge

Нужно указать для своего профиля в ejudge имя, фамилию и группу!

Как это сделать, описано здесь.

3. Форматирование строк

Придётся забежать вперёд и кое-что сказать про способы задания строк; это нам понадобится в примерах.

Обращаю внимание: все приведённые ниже примеры - обыкновенные строки; разница лишь в том, как мы собираемся их интерпретировать.

3.1. Интерпретация строковых литералов

Обычные строки, на самом деле, обрабатываются питоном.

   1 >>> "XKCD"    # Строка как строка
   2 'XKCD'
   3 >>> "XK\nCD"    # \n в строке интерпретируется как символ переноса строки; т.е. данная строка состоит из 5 символов, а не 6
   4 'XK\nCD'
   5 >>> print("XK\nCD")    # Что и видим
   6 XK
   7 CD
   8 >>> len("XK\nCD")
   9 5
  10 

3.2. Модификатор r

Подобной интерпретации можно избежать, указав перед строкой модификатор r (от "raw", такие строки называют сырыми):

   1 >>> print(r"XK\nCD")    # Здесь \n интерпретируется уже как два символа, а не один
   2 XK\nCD
   3 >>> len(r"XK\nCD")    # Поэтому такая строка состоит из 6 символов, а не 5 (это всё ещё обычная строка)
   4 6
   5 

С модификатором r есть некоторые тонкости, но это уже совсем другая история.

3.3. Форматные строки (модификатор f)

В python 3.6 (в системе ejudge пока установлен только python 3.5) появилась такая штука, как форматированные строковые литералы (форматные строки).

По сути, она говорит, что строку нужно интерпретировать ещё сильнее.

   1 >>> print(f"XK{1234 + 8765}CD")    # В фигурных скобках указывается некоторое питоновское выражение; таким образом, строка в прямом смысле слова вычисляется во время выполнения программы
   2 XK9999CD
   3 

Опасная ли это штука? В общем случае - да:

   1 >>> f"{dir()}"    # По сути, получилась та же опасность, что с eval
   2 "['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__']"
   3 

Учитывая, что есть функция (не оператор) import, можно творить разнообразные "чудеса".

3.4. Cтроковый метод format()

Пока на сервере ejudge ещё не python 3.6, давайте посмотрим, как форматировать строки без форматных строк.

С этой задачей справляется строковый метод format. Единственное его отличие - он не умеет делать eval. (Собственно, все вычисляемые выражения указываются как аргументы функции, а не как части строки.)

   1 >>> "---{}//{}---".format(123,456)    # Используем фигурные скобки для указания, куда подставлять аргументы метода
   2 '---123//456---'
   3 >>> "---{1}/{0}/{1}---".format(123,456)    # Можно указать номер аргумента (начиная с нуля) и явно
   4 '---456/123/456---'
   5 >>> "---{1:6}/{0:6}/{1:2}---".format(123,456)    # И применять разные опции форматирования (здесь - ширина строки в символах)
   6 '---   456/   123/456---'
   7 

3.5. Форматные символы

Остановимся на них подробнее, когда будем разбираться со строками; пока что же нам достаточно знать, что \n - это перенос строки.

4. Циклы

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

Поэтому в питоне, разумеется, есть операторы цикла - целых два.

4.1. Цикл while

4.1.1. Цикл со счётчиком

Первый вид циклов в питоне - самый простой - цикл while. Он задаёт блок действий, который нужно выполнять до тех пор, пока выполняется некое условие:

   1 >>> i = 0
   2 >>> while i < 10:    # После while указывается условие, а затем ставится двоеточие
   3 ...     print(i)    # Тело цикла отделяется отступом в 4 пробела
   4 ...     i += 1
   5 ...
   6 0
   7 1
   8 2
   9 3
  10 4
  11 5
  12 6
  13 7
  14 8
  15 9
  16 

Обратите внимание на три вещи:

  1. Мы реализовали примитивную рекурсию - т.е. цикл со счётчиком; с помощью арифметических операций мы можем заранее предсказать, сколько итераций будет в цикле.
  2. В любом цикле есть четыре секции:

    1. инициализация - присваивание начальных значений переменным, которые потом будем проверять;

    2. проверка свойств данных (проверка условия);

    3. какое-то тело - собственно, что нам нужно сделать;

    4. изменение свойств данных, которые вы потом будете проверять (изменение значений проверяемых переменных);

  3. Рекурсия - штука гораздо более гибкая, чем цикл (за счёт того, что при вызове создаётся собственное пространство имён, в частности). Цикл же может лишь циклически выполнять своё тело, пока выполнено условие.

4.1.2. Замечание о пространствах имён

В питоне ни в условном операторе, ни в операторе цикла никаких собственных пространств имён не создаётся - в отличие от С/С++.

4.1.3. Подобие общей рекурсии

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

Самый простой пример - цикл, который проверяет ввод.

   1 a = input()    # Инициализация цикла
   2 while a:
   3     print("//", a)
   4     a = input()    # Можно ли было не писать input() дважды, а встроить "a = input()" прямо в тело цикла? Нет; дальше объясню, почему.

Мы получили цикл, который выполняется неизвестно сколько раз:

   1 $ python3 cycle.py
   2 It's just Rick and Morty
   3 // It's just Rick and Morty
   4 Rick and Morty and their adventures, Morty
   5 // Rick and Morty and their adventures, Morty
   6 RICK AND MORTY FOREVER AND FOREVER A HUNDRED YEARS Rick and Morty.. some...things..
   7 // RICK AND MORTY FOREVER AND FOREVER A HUNDRED YEARS Rick and Morty.. some...things..
   8 Me and Rick and Morty runnin' around and...
   9 // Me and Rick and Morty runnin' around and...
  10 Rick and Morty time...
  11 // Rick and Morty time...
  12 a- all day long forever.. all a - a hundred days Rick and Morty!    # Понимаете, продолжать можно сколько угодно
  13 //  a- all day long forever.. all a - a hundred days Rick and Morty!

Обратите внимание на операторы input(): в конструкции цикла while нам приходится разделять инициализацию и повторяющийся ввод.

PEP, предлагающий цикл с постусловием, имел место, но так и не был реализован1.

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

4.1.4. Вложенные циклы

Ничего сверхудивительного я тут не расскажу; давайте сразу перейдём к примеру - выведем небольшую таблицу умножения:

   1 N = 4    # Таблица умножения размером (N - 1) * (N - 1)
   2 i, j = 1,1     # Инициализируем счётчики
   3 while i < N:    # Проходим по строкам
   4     while j < N:     # А, точнее, по всем столбцам каждой строки
   5         print(f"{i}*{j}=={i*j}", end=" ")    # Обратите внимание, мы передаём функции print параметр end, определяющий, каким символом завершить напечатанное
   6         j += 1    # Счётчик столбцов
   7     print("")    # Переход на следующую строку
   8     i += 1    # Счётчик строк

   1 $ python3 cycle.py    # Догадались, почему вышло не то, что ожидали?
   2 1*1==1 1*2==2 1*3==3

Инициализацию для вложенного цикла мы внесли не туда, куда нужно. Её нельзя отрывать от вложенного цикла:

   1 N = 4
   2 i = 1
   3 while i < N:
   4     j = 1    # Вот где должна была быть инициализация j
   5     while j < N:
   6         print(f"{i}*{j}=={i*j}", end=" ")
   7         j += 1
   8     print("")
   9     i+=1

   1 $ python3 cycle.py
   2 1*1==1 1*2==2 1*3==3
   3 2*1==2 2*2==4 2*3==6
   4 3*1==3 3*2==6 3*3==9

4.2. Управление выполнением цикла

4.2.1. Оператор continue

Оператор continue позволяет перейти сразу на следующую итерацию цикла (внезапно).

Выведем на печать несколько чисел, но пропустим число 13:

   1 i = 5
   2 while i < 20:
   3     i += 2
   4     if i == 13:
   5         continue     # Пропустим печать числа
   6     print(i)

   1 $ python3 cycle.py
   2 7
   3 9
   4 11
   5 15
   6 17
   7 19
   8 21

4.2.2. Оператор break

Оператор break позволяет прервать выполнение цикла и продолжить выполнение кода с того места, где описание цикла закончилось.

Реализуем алгоритм под названием "поиск первого"; программа будет считывать с клавиатуры числа до тех пор, пока не встретит отрицательное или пока не закончится ввод (признаком конца ввода послужит ноль):

   1 n = int(input())
   2 was = False    # Переменная - флаг того, что мы вышли из цикла именно с помощью break, а не просто из-за того, что ввод закончился
   3 
   4 while n:    # Пока не получили с ввода число 0
   5     if n < 0:        # Встретили отрицательное число - можно выходить
   6         print("Было")
   7         was = True
   8         break
   9     n = int(input())    # Если же последнее число было положительным, продолжаем ввод
  10 
  11 if not was:    # Проверяем флаг
  12     print("Не было")

   1 $ python3 cycle.py
   2 1
   3 2
   4 3
   5 0
   6 Не было

   1 $ python3 cycle.py
   2 2
   3 -1
   4 Было

4.2.2.1. Сахар для break: ветка else

Проверка флага для того, чтобы понять, был ли выход из цикла осуществлён потому, что условие больше не выполняется, очень популярный шаблон.

Настолько популярный, что в питоне введён специальный clause (ветка) для этого (чтобы не придумывать дополнительные флажки, как в коде выше).

Эту ветку, ветку else, можно пристроить к любому циклу (while в частности):

   1 n = int(input())
   2 
   3 while n:
   4     if n < 0:
   5         print("Было")
   6         break
   7     n = int(input())
   8 else:    # Что делать, если мы вышли из цикла не по break, а естественным образом
   9     print("Не было")

Мы обошлись без введения дополнительных проверок и без введения дополнительных объектов, потому что интерпретатор знает, по какой причине мы на самом деле вышли из цикла.

4.3. Цикл for (проходящий по последовательности)

Второй вид цикла в питоне - цикл, который проходит по последовательности. Мы с вами знаем уже как минимум две последовательности - строки и кортежи. Проход по последовательности выглядит так:

for [имя] in [последовательность]:
    [тело с отступом в 4 пробела]

На разных итерациях цикла указанным именем будут связаны поочерёдно элементы указанной последовательности. Например:

   1 >>> for n in (1,45,67,345,78,34):    # n поочерёдно будет связано со значениеми 1, 45, 67, 345, 78, 34
   2 ...     print(n)
   3 ...
   4 1
   5 45
   6 67
   7 345
   8 78
   9 34
  10 >>> for c in "SPELLFORCE":    # Забавный факт: строка - последовательность... строк единичной длины. Мы об этом ещё поговорим отдельно на занятии, посвящённом строкам.
  11 ...     print(c)
  12 ...     print(type(c))
  13 ...     print(len(c))
  14 ...
  15 S
  16 <class 'str'>
  17 1
  18 P
  19 <class 'str'>
  20 1
  21 E
  22 <class 'str'>
  23 1
  24 L
  25 <class 'str'>
  26 1
  27 L
  28 <class 'str'>
  29 1
  30 F
  31 <class 'str'>
  32 1
  33 O
  34 <class 'str'>
  35 1
  36 R
  37 <class 'str'>
  38 1
  39 C
  40 <class 'str'>
  41 1
  42 E
  43 <class 'str'>
  44 1
  45 

Обратите внимание, что это не цикл со счётчиком; это цикл прохода последовательности. Любая последовательность, т.е. конструкция, поддерживающая операцию итерирования, может быть засунута в правую часть цикла for.

4.3.1. Внутреннее устройство for

4.3.1.1. Роль итератора

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

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

Проверка условия - проверка того, что последовательность не закончилась. Проверка условия в for - на самом деле, появление StopIteration при использовании нашего итератора.

Изменение состоит в том, что мы берём следующее значение итератора.

Если вы запутались, в чём разница между генератором и итератором, этот пост вам поможет.

Создадим какой-нибудь генератор, чтобы увидеть, что цикл for работает для него совершенно аналогично:

   1 >>> def sg(n):    # sg - simple generator
   2 ...     i = 0
   3 ...     while i < n:
   4 ...         yield i
   5 ...         i += 1
   6 ...
   7 >>> g = sg(4)    # Вспоминаем, как работать с генераторами
   8 >>> g
   9 <generator object sg at 0x000002196493E4C0>
  10 >>> next(g)    # Строго говоря, next() - всего лишь обёртка для вызова метода __next__, который есть не только у генераторов, но и у итераторов вообще
  11 0
  12 >>> next(g)
  13 1
  14 >>> next(g)
  15 2
  16 >>> next(g)
  17 3
  18 >>> next(g)    # Пресловутый StopIteration
  19 Traceback (most recent call last):
  20   File "<stdin>", line 1, in <module>
  21 StopIteration
  22 >>> f = sg(4)
  23 >>> for i in f:    # Точно так же пройдёмся по генератору, как по кортежу или по строке (потому что генератор - ничто иное, как вычислимая последовательность)
  24 ...     print(i)
  25 ...
  26 0
  27 1
  28 2
  29 3
  30 >>> next(f)    # f сам и выступил в качестве итератора, который заводится при инициализации в цикле for, потому исчерпал свой ресурс
  31 Traceback (most recent call last):
  32   File "<stdin>", line 1, in <module>
  33 StopIteration

4.3.1.2. Метод __iter__

В действительности выражение, стоящее в правой части цикла for, обязано поддерживать iteration protocol; мы должны уметь изготавливать из него итератор.

Если у вашего объекта есть метод __iter__, он может участвовать в правой части цикла for. Такой объект называется итерируемым (iterable) - его метод __iter__ вернёт итератор.

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

   1 >>> dir("SDFSDF")    # Например, у строки есть метод __iter__
   2 ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill']
   3 >>> I = iter("SDFSDF")    # iter(a) - обёртка для вызова a.__iter__(); получим итератор для строки
   4 >>> for c in I:    # В цикле for при инициализации, вообще говоря, выполнился вызов iter(I); однако I - итератор, поэтому этот вызов вернул сам объект I
   5 ...     print(c)
   6 ...
   7 S
   8 D
   9 F
  10 S
  11 D
  12 F
  13 >>> I = iter("SDFSDF")    # Ещё раз продемонстрируем разницу между итеруемыми объектами и итераторами
  14 >>> I == "SDFSDF"    # Строка - итерируемый объект, поскольку от неё можно вызвать iter() (она имеет метод __iter__); но сама она итератором не является
  15 False
  16 >>> type(I)
  17 <class 'str_iterator'>
  18 >>> I == iter(I)    # Итератор - тоже итерируемый объект (у него есть метод __iter__), и при этом он сам себе итератор
  19 True
  20 >>> I is iter(I)    # Метод __iter__ итератора возвращает не новый объект, как, скажем, для строки, а сам этот итератор
  21 True
  22 >>> I = sg(4)    # Генератор - частный случай итератора, поэтому у него тоже есть метод __iter__, который тоже возвращает сам объект, от которого вызван - т.е. тот же самый генератор
  23 >>> I == iter(I)
  24 True
  25 >>> I is iter(I)
  26 True
  27 

4.3.2. Нормальные циклы со счётчиком: range

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

И нужный для этого тип данных в питоне есть - называется он range (диапазон). Классический цикл со счётчиком в питоне выглядит вот так:

   1 >>> for n in range(5):
   2 ...     print(n)
   3 ...
   4 0
   5 1
   6 2
   7 3
   8 4
   9 

Как нетрудно догадаться, range - это такая последовательность. Что про неё можно сказать?

  1. она итерируемая (у range есть метод __iter__);

  2. задаётся range с помощью трёх параметров: начального значения, конечного значения (оно само в последовательность не входит) и шага.

   1 >>> range(5)    # По умолчанию шаг считается равным 1, а начальное значение - равным 0; таким образом, задание range от одного параметра определяет лишь правую границу диапазона
   2 range(0, 5)
   3 >>> range(-2)    # Которая не обязана быть больше левой границы
   4 range(0, -2)
   5 >>> for i in range(-2):    # Но и поведение в таком случае будет соответствующим: если правая граница диапазона не правее левой, то элементов в диапазоне попросту нет
   6 ...     print(i)
   7 ...
   8 >>> range(-10, 20)    # Границы диапазона могут быть и отрицательными числами; range от двух параметров - range с заданными левой и правой границами и шагом 1
   9 range(-10, 20)
  10 >>> for i in range(1, 5):    # Напоминаю, что правая граница в диапазон не входит
  11 ...     print(i)
  12 ...
  13 1
  14 2
  15 3
  16 4
  17 >>> for i in range (1, 1):    # Ни при каких обстоятельствах
  18 ...     print(i)
  19 ...
  20 >>> for i in range (1, 11, 2):    # Если указаны все три параметра, то третий из них задаёт шаг, с котором будут браться элементы из диапазона, начиная с левой границы
  21 ...     print(i)
  22 ...
  23 1
  24 3
  25 5
  26 7
  27 9
  28 >>> for i in range(100, 0, -10):    # Шаг может быть и отрицательным (понятия "левая" и "правая" границы в таком случае должны, по идее, меняться местами и не совсем уместны, поэтому правильнее говорить о начале и конце диапазона)
  29 ...     print(i)
  30 ...
  31 100
  32 90
  33 80
  34 70
  35 60
  36 50
  37 40
  38 30
  39 20
  40 10
  41 

Самое интересное, что range ещё и индексируемый:

   1 >>> r = range(5)    # range от нуля до 5 (не включая) с шагом 1
   2 >>> r[0]    # Можно брать его элементы по индексу
   3 0
   4 >>> r[2]
   5 2
   6 >>> r[-1]    # Причём индекс может быть и отрицательным: последний элемент имеет индекс -1, предпоследний - -2, и так далее
   7 4
   8 >>> r[10]    # Попытка получить элемент по несуществующему индексу приведёт к ошибке
   9 Traceback (most recent call last):
  10   File "<stdin>", line 1, in <module>
  11 IndexError: range object index out of range
  12 >>> r[-10]    # Будь то положительный или отрицательный индекс
  13 Traceback (most recent call last):
  14   File "<stdin>", line 1, in <module>
  15 IndexError: range object index out of range
  16 >>> r[5]    # Кстати, очевидный факт, но всё же: индекса, равного количеству элементов в диапазоне, не существует, поскольку нумерация элементов начинается с нуля
  17 Traceback (most recent call last):
  18   File "<stdin>", line 1, in <module>
  19 IndexError: range object index out of range
  20 >>> r[-6]    # Для отрицательных индексов, соответственно, с минус единицы
  21 Traceback (most recent call last):
  22   File "<stdin>", line 1, in <module>
  23 IndexError: range object index out of range

Кстати, range - хороший пример объекта в питоне, который не хранится в памяти (он вычислимый) и при этом является индексируемым:

   1 >>> r = range(-100500, 100500, 5)    # Нет, мы не будем хранить 40200 объектов в памяти одновременно
   2 >>> r[40199]    # Но использовать индексы можем так, будто храним
   3 100495
   4 

5. Индексация в python

5.1. Обычная индексация

Всё, что мы сказали выше про индексацию в range, справедливо и для многих других типов в python - для списков, кортежей, строк и т.д.

   1 >>> c = 1,2,3,4,5,6,7,8,9    # Обычный кортеж; обратите внимание, я не использую скобки там, где это необязательно
   2 >>> c[0]     # Обычная индексация
   3 1
   4 >>> c[8]
   5 9
   6 >>> c[-3]    # Можно брать и отрицательные индексы; здесь - третий с конца элемент
   7 7
   8 >>> c[9]    # Тоже нет индекса, равного количеству содержащихся элементов, и тоже получим ошибку при использовании несуществующего индекса
   9 Traceback (most recent call last):
  10   File "<stdin>", line 1, in <module>
  11 IndexError: tuple index out of range
  12 >>> c[-10]    # Будь он положительный или отрицательный
  13 Traceback (most recent call last):
  14   File "<stdin>", line 1, in <module>
  15 IndexError: tuple index out of range

По сути, вы можете пользоваться кортежами как массивами:

   1 >>> for i in range(len(c)):    # Вот так, с помощью нехитрых приспособлений, буханку белого (или чёрного) ХЛЕБА можно превратить в троллейбус... Но зачем?!
   2 ...     print(i, c[i])
   3 ...
   4 0 1
   5 1 2
   6 2 3
   7 3 4
   8 4 5
   9 5 6
  10 6 7
  11 7 8
  12 8 9
  13 

Открою вам секрет: в действительности кортеж - это и есть массив, а, точнее, таблица. Кортеж - это массив, в котором хранятся идентификаторы объектов, которые ссылаются, собственно, на сами объекты.

Поcкольку кортеж - неизменямый тип данных (один раз инициализировали и поменять не можем), ничего не стоит завести в памяти такую таблицу. Поэтому операция индексации в кортежах очень дешёвая.

Такая же штука со строками, только строки несколько более извращённый тип данных; как мы уже говорили, результат операции индексирования для строки - строка из одного символа.

   1 >>> s = "sometext"
   2 >>> s[3]
   3 'e'
   4 >>> type(s)
   5 <class 'str'>
   6 >>> type(s[3])
   7 <class 'str'>
   8 

5.2. Автоматическая индексация при проходе по последовательности: enumerate

5.2.1. Суть проблемы

   1 >>> for i in range(len(c)):    # Разберём подробнее этот пример
   2 ...     print(i, c[i])
   3 ...
   4 0 1
   5 1 2
   6 2 3
   7 3 4
   8 4 5
   9 5 6
  10 6 7
  11 7 8
  12 8 9
  13 

Что мне не нравится в цикле выше (где мы обращались к кортежу как к массиву):

  1. использование индексирования как отдельной операции в print;

  2. явный вызов функции len, чтобы превратить проход по самому кортежу в проход по range'у от него.

Есть более интересный и более питонистый способ пройтись по последовательности, и при этом знать индекс текущего элемента. Но сначала придётся сделать небольшое отступление.

5.2.2. Распаковка в for

Если ваша последовательность в качестве элементов содержит последовательности (скажем, пары элементов), то при проходе по ней вы можете использовать операцию распаковки:

   1 >>> cc = (1,2), (3,4), (100,500)    # Кортеж кортежей
   2 >>> for a, b in cc:    # Т.к. каждый элемент кортежа cc - кортеж-пара из двух элементов, мы можем распаковать каждый такой элемент с помощью двух переменных a и b
   3 ...     print(b, a)
   4 ...
   5 2 1
   6 4 3
   7 500 100
   8 

Т.е. в цикле for вы можете, как при множественном связывании (это оно и есть), указать несколько имён, которые будут связывать элементы последовательности, если эти элементы сами являются последовательностями.

Есть даже операция распаковки с плейсхолдером, *, в который приедет всё, что не было распаковано явно:

   1 >>> ccc = (1,2,3,4), ("a", "bb", "ccc", "dddd"), ((), 0, "", None)    # Добавим побольше элементов в каждый элемент последовательности ccc, чтобы показать, как работает плейсхолдер
   2 >>> for a, *b, c in ccc:    # b указано со звёздочкой, плейсхолдером, поэтому в b будет приезжать всё, что не влезло в a и c
   3 ...     print(b, a, c)
   4 ...     print(type(b))    # Правда, приезжать оно будет в виде списка, с которыми мы с вами ещё не знакомились; считайте пока, что это кортеж (в данном случае разницы не будет)
   5 ...
   6 [2, 3] 1 4
   7 <class 'list'>
   8 ['bb', 'ccc'] a dddd
   9 <class 'list'>
  10 [0, ''] () None
  11 <class 'list'>
  12 

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

5.2.3. enumerate

Так вот чтобы пройтись по последовательности, зная индекс текущего элемента, существует специальный тип - enumerate.

enumerate() от последовательности - это последовательность пар вида (индекс, элемент исходной последовательности), и пользуются этим как раз с помощью распаковки в цикле for:

   1 >>> e = enumerate((5,6,7))    # Ну-ка, преврати кортеж из трёх элементов в последовательность из трёх пар индекс-значение
   2 >>> e    # enumerate - отдельный тип, являющийся последовательностью, по которой можно проводить итерацию
   3 <enumerate object at 0x0000021964AE8708>
   4 >>> I = iter(e)    # Получим итератор от нашего enumerate, чтобы увидеть, как он устроен изнутри
   5 >>> next(I)    # Ну да, всего лишь последовательность пар индекс-значение
   6 (0, 5)
   7 >>> next(I)
   8 (1, 6)
   9 >>> next(I)
  10 (2, 7)
  11 >>> next(I)
  12 Traceback (most recent call last):
  13   File "<stdin>", line 1, in <module>
  14 StopIteration

Давайте, наконец, исправим наш злополучный цикл (в качестве напоминания я приведу его исходный код ещё раз):

   1 >>> c = 1,2,3,4,5,6,7,8,9
   2 >>> for i in range(len(c)):    # Вот такой проход по кортежу
   3 ...     print(i, c[i])
   4 ...
   5 0 1
   6 1 2
   7 2 3
   8 3 4
   9 4 5
  10 5 6
  11 6 7
  12 7 8
  13 8 9
  14 >>> for i, val in enumerate(c):    # Можно было представить гораздо более понятно
  15 ...     print(i, val)
  16 ...
  17 0 1
  18 1 2
  19 2 3
  20 3 4
  21 4 5
  22 5 6
  23 6 7
  24 7 8
  25 8 9
  26 

5.2.4. Плюсы enumerate

  1. Это более питонистый путь;
  2. такой код лучше читается;
  3. enumerate позволяет избегать операции индексирования, которая в некоторых случаях является существенно дорогой.

5.3. Операция in

Помимо всего прочего, есть операция in, которая, по крайней мере, в случае кортежа или списка, тоже приводит к итерации по последовательности (для них она, по сути, реализует алгоритм поиска первого):

   1 >>> c = 1,2,34,5,6,78,9
   2 >>> 100500 in c
   3 False
   4 >>> 34 in c
   5 True
   6 

Для строк ситуация немного другая, но об этом мы потом поговорим отдельно (там in выполняет поиск подстроки в строке).

6. Секционирование последовательности: срезы

Ремарка: в обсуждениях и на русском, и на английском языке слово "slice" (срез) используется неоднозначно; под ним подразумевается одно или оба из понятий:

  1. подпоследовательность последовательности - в этом смысле срезом строки "Monty Python" может быть "ty Pyt";
  2. объект типа slice (что это и зачем - выясним ниже).

Ситуация усугубляется тем, что операция взятия среза в первом смысле - по сути, всего лишь синтаксический сахар для использования среза во втором смысле. Мы постараемся слово "срез" использовать в смысле первого определения, а объект типа slice будем называть слайсом. Хоть и масло масляное, но хоть как-то формально границу между понятиями проведём.

6.1. Суть

Переходим к ещё более интересной теме: секционирование последовательности.

Идея секционирования проста: вы можете взять из последовательности подпоследовательность.

Делается это с помощью таких же квадратных скобок, как при операции индексирования, только синтаксис выглядит иначе:

    имя_последовательности[индекс_начала:индекс_конца:шаг]

6.2. Нюансы

Шаг вместе с предшествующим ему двоеточием можно не указывать, тогда шаг принимается равным 1. А вот первое двоеточие придётся указать в любом случае.

Элемент, индекс которого совпадает с индексом_конца, в срез уже не попадает (можно провести аналогию с range).

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

Если не указать индекс_конца, то по умолчанию он будет соответствовать последнему элементу плюс один (если шаг положительный) или элементу, стоящему перед нулевым (не минус первому!) (если шаг отрицательный).

Как известно, лучше один раз увидеть, чем тысячу раз услышать:

   1 >>> s = tuple(range(10,60,6))    # Для примера будем брать срезы (подпоследовательности) кортежа
   2 >>> s
   3 (10, 16, 22, 28, 34, 40, 46, 52, 58)
   4 >>> s[1:3]    # Срез кортежа (тоже кортеж): элементы с первого (нумерация с нуля) до второго включительно
   5 (16, 22)
   6 >>> s[5:8]    # Элементы с пятого до седьмого включительно (опять-таки, нумерация с нуля)
   7 (40, 46, 52)
   8 >>> s[4:4]    # Элемент с индексом, совпадающим с индексом конца среза, в срез не попадает
   9 ()
  10 >>> s[:3]    # Все элементы от начала кортежа до третьего элемента, не включая его
  11 (10, 16, 22)
  12 >>> s[6:]    # Все элементы, начиная с шестого и до конца
  13 (46, 52, 58)
  14 >>> s[:]    # Кортеж из всех элементов данного
  15 (10, 16, 22, 28, 34, 40, 46, 52, 58)
  16 >>> s is s[:]    #  Для кортежей такая операция взятия среза вернёт тот же самый кортеж, поскольку кортеж - неизменяемый тип данных, и нет смысла заводить вторую его копию в памяти. А вот, скажем, для списков будет возвращён новый объект; этим часто пользуются, например, для чтобы обнуления списка: s = s[:]
  17 True
  18 >>> s[0:6:2]    # Срез из элементов, имеющих индекс из диапазона 0-6, не включая правую границу, взятых с шагом 2
  19 (10, 22, 34)
  20 

И индексы начала и конца, и шаг могут быть отрицательными:

   1 >>> s    # Напоминаю, с какой последовательностью мы работаем
   2 (10, 16, 22, 28, 34, 40, 46, 52, 58)
   3 >>> s[-1:0]    # Срез "от последнего элемента до нулевого" всегда будет пустым
   4 ()
   5 >>> s[-4:6]    # Срез "от четвёртого элемента с конца до шестого" (обратите внимание на повисшую запятую (trailing comma) в конце кортежа)
   6 (40,)
   7 >>> s[0:-3]    # Срез "от нулевого элемента до третьего с конца"
   8 (10, 16, 22, 28, 34, 40)
   9 >>> s[0::-2]    # Интересный пример: срез "от нулевого элемента до стоящего перед нулевым с шагом -2"
  10 (10,)
  11 >>> s[6::-2]    # Иногда проще думать о срезах не в терминах индексов: срез из элементов от нулевого до седьмого включительно, взятых в обратном порядке с шагом 2, начиная с шестого
  12 (46, 34, 22, 10)
  13 >>> s[6:0:-2]    # Обратите внимание, что, если не указывать индекс конца при отрицательном шаге, как в примере выше, срез берётся не до нулевого элемента, а как бы до элемента перед нулевым (называть его "минус первым" будет некорректно, поскольку элемент с индексом -1 существует  и обозначает совсем другое)
  14 (46, 34, 22)
  15 >>> s[::-2]    # Снова, не будем думать об индексах: "срез из элементов кортежа, взятых в обратном порядке с шагом два, начиная с последнего"
  16 (58, 46, 34, 22, 10)
  17 

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

   1 >>> s[-500:-100]    # Задаваемый диапазон вообще не пересекается с диапазоном индексов нашего кортежа
   2 ()
   3 >>> s[6:1024]    # Частично пересекается
   4 (46, 52, 58)
   5 >>> s[-42:42]    # Диапазон индексов кортежа целиком содержится в том диапазоне, по которому мы хотим взять срез
   6 (10, 16, 22, 28, 34, 40, 46, 52, 58)
   7 

С учётом всех тонкостей операции взятия среза, не лишним будет проиллюстрировать, как определить нужный индекс:

slice.PNG

6.3. Под капотом: slice

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

Ещё я уже намекал, что в питоне есть только методы объектов, а все остальные функции, которые мы рассматриваем - это всего лишь синтаксический сахар.

Операция секционирования и операция индексирования - на самом деле один и тот же метод под названием __getitem__.

   1 >>> s    # Посмотрим, скажем, на уже знакомый нам кортеж
   2 (10, 16, 22, 28, 34, 40, 46, 52, 58)
   3 >>> dir(s)    # У него метод __getitem__ есть, значит, его можно индексировать и от него можно брать срезы
   4 ['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'count', 'index']
   5 >>> s[3]    # Вот это
   6 28
   7 >>> s.__getitem__(3)    # На самом деле, всего лишь синтаксический сахар для вот этого
   8 28
   9 

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

Методу __getitem__ вместо числа - индекса - подаётся объект специального типа - slice:

   1 >>> sl = slice(1,4)    # Указали начальный и конечный индексы
   2 >>> sl    # None здесь - это шаг (мы его не указали при создании слайса); с тем же успехом мы могли явно задать шаг "1"
   3 slice(1, 4, None)
   4 >>> r = range(1, 4)    # Напоминает range? Это потому, что при создании слайса и используется range
   5 >>> r
   6 range(1, 4)
   7 >>> s[1:4]    # Операция взятия среза
   8 (16, 22, 28)
   9 >>> ss = s.__getitem__(sl)    # На самом деле, всего лишь обёртка вокруг __getitem__
  10 >>> ss
  11 (16, 22, 28)
  12 >>> s[sl]    # Можно даже так
  13 (16, 22, 28)
  14 

При реализации своих классов вы можете организовать такой же сахар: добавьте метод __getitem__, делайте в нём проверку того, объект какого типа вам приехал в качестве параметра и, в зависимости от этого, реализуйте различное поведение.

6.3.1. Вопрос на миллион

Почему slice может быть передан в __getitem__, а range - нет?

   1 >>> s
   2 (10, 16, 22, 28, 34, 40, 46, 52, 58)
   3 >>> ss = slice(1,4)
   4 >>> s.__getitem__(ss)
   5 (16, 22, 28)
   6 >>> r = range(1,4)
   7 >>> s.__getitem__(r)    # Ни напрямую
   8 Traceback (most recent call last):
   9   File "<stdin>", line 1, in <module>
  10 TypeError: tuple indices must be integers or slices, not range
  11 >>> ss = slice(r)    # Ни даже после преобразования в слайс
  12 >>> s.__getitem__(ss)
  13 Traceback (most recent call last):
  14   File "<stdin>", line 1, in <module>
  15 TypeError: slice indices must be integers or None or have an __index__ method

Тривиальный ответ известен: потому что так описаны соответствующие классы. Но в чём причина такого поведения?

Спойлер (нажмите «комментарии» наверху страницы, чтобы посмотреть): -- FrBrGeorge 2018-10-19 13:25:17

Если вы знаете ответ, напишите на почту: romansdidnotcrucify@gmail.com

6.4. ellipsis (...)

Помните я говорил, что в питоне есть языковые конструкции, которые не поддерживаются ни одним из стандартных объектов питона (но могут использоваться в сторонних модулях)?

Например, перемножение матриц (мы на него ещё посмотрим):

   1 >>> @

Для срезов тоже есть такая конструкция - ... (ellipsis):

   1 >>> ...
   2 Ellipsis
   3 >>> s[...]    # Обратите внимание: мы получили ошибку TypeError, а не SyntaxError: значит, мы лишь промахнулись с типом объекта, для которого применили операцию, но не с самим выражением
   4 Traceback (most recent call last):
   5   File "<stdin>", line 1, in <module>
   6 TypeError: tuple indices must be integers or slices, not ellipsis

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

Для демонстрации работы ellipsis нам потребуется модуль numpy, широко используемый в вычислениях на питоне (если он у вас ещё не установлен, здесь подскажут, как это сделать).

   1 >>> import numpy    # Импортировали модуль numpy и теперь можем создавать матрицы
   2 >>> a = numpy.array([\
   3 ...     [1,2,3],\
   4 ...     [4,5,6],\
   5 ...     [7,8,9],\
   6 ...     [10,11,12]])
   7 >>> a[...,2]    # Ellipsis, по сути, означает "все возможные значения". Здесь получим второй (нумерация с нуля) столбец
   8 array([ 3,  6,  9, 12])
   9 >>> a[...,1]    # Первый столбец ("взять элементы любой строки, первого столбца")
  10 array([ 2,  5,  8, 11])
  11 >>> a[1,...]    # Первая строка ("взять элементы первой строки, любого столбца")
  12 array([4, 5, 6])
  13 >>> a[1:,...]    # Здесь первым параметром указали не индекс, а срез (вернее, сказали, что нужно взять срез): "взять элементы строк с первой и до конца, любого столбца"
  14 array([[ 4,  5,  6],
  15        [ 7,  8,  9],
  16        [10, 11, 12]])
  17 >>> a[...,:2]    # "Взять элементы любой строки, любого столбца до второго"
  18 array([[ 1,  2],
  19        [ 4,  5],
  20        [ 7,  8],
  21        [10, 11]])
  22 >>> b = a[1:,...]    # Кстати, продемонстрируем, как перемножать матрицы
  23 >>> c = a[:-1,...]    # При этом всё, что мы сейчас увидели - это всё тот же __getitem__, только более хитрый; здесь, например, он получает в качестве параметров слайс и ellipsis
  24 >>> b @ c    # И получились две вполне валидные матрицы размера 3 x 3, которые можно перемножить
  25 array([[ 66,  81,  96],
  26        [102, 126, 150],
  27        [138, 171, 204]])
  28 


  1. Человек, предложивший его, не смог описать Гвидо адекватный способ указания постусловия с учётом отступа тела цикла. В итоге решили не плодить сущности и не внедрять новую языковую конструкцию. (1)

LecturesCMC/PythonIntro2018/04_CircleSequence/Conspect (последним исправлял пользователь FrBrGeorge 2018-10-19 13:26:23)