04. Цикл for. Функции range, enumerate, zip, map. Генераторы.


Цикл `for`

На предыдущем занятии мы кратко ознакомились с циклом for DOCS. Давайте вспомним как выглядит конструкция этого цикла:

for X in SEQUENCE:
    БЛОК КОДА
    БЛОК КОДА
    БЛОК КОДА

Как и в цикле while или условной конструкции if, блок кода имеет отступы в 4 пробела (обычно они ставятся автоматически, после переноса с первой строки). В отличие от цикла while, мы делаем заранее изместное и нами заданное число итераций, а не "пока условие выполнится". Мы проходимся по элементам последовательности SEQUENCE, которая может быть задана разными способами, основные из них мы сегодня изучим! В блоке кода может быть другой цикл, который будет называться вложенным циклом, для названия переменной цикла которого лучше всегда (если только это специально не предусмотрено) использовать новое имя.

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

для (for) каждого $x_1, x_2, \dots, x_n$ в (in) последовательности $X$ выполни код

Подробнее как происходят итерации цикла на диаграмме:

диаграмма цикла for пошагово

Очень важно понять, что:

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

Как и для цикла while, здесь работают команды break и continue. Первая, прерывает работу цикла и выходит из него, вторая прерывает итерацию цикла и переходит к следующей итерации.

Итерирование по элементам

Например, пройдемся по элементам списка:

In [ ]:
spisok = [1, 2, 3]

print('--- до цикла     ---')
x = -10
print('x =', x)
print('--- внутри цикла ---')
for x in spisok:
    print('x =', x)
    x = x + 2
    print('---> x =', x)
print('--- после цикла  ---')
print('x =', x)

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

In [ ]:
alphabet = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'

for letter in alphabet:
    print(letter, letter.lower(), sep='', end='|')
In [ ]:
for el in 'kaliningrad', 'moscow', 'ufa', 'chelybinsk', 'vladivostok':
    print(el[0].upper() + el[1:])

Итерирование по индексам

Часто решение задачи требует знания номера элемента, поэтому итерирования по элементам будет недостаточно (хотя всегда можно воспользоваться методом .index(), но это очень неэффективный код -- почему?).

Что бы мы хотели иметь для итерирования по индексам? Рассмотрим пример:

In [ ]:
a =       ['A', 'B', 'C', 'D', 'E', 'F']
indices = [  0,   1,   2,   3,   4,   5]
for i in indices:
    if (i + 1) % 2:
        print(a[i], '- нечетная буква')
    else:
        print(a[i], '- четная буква')

Последовательность чисел `range`

Чтобы создать последовательность чисел, можно воспользоваться функцией range() DOCS. Её устройство схоже со сложными срезами, которые мы изучили на прошлом занятии, а именно, синтаксис функции выглядит так:

range([start=0], stop, [step=1]) 
    := start, start + step, ..., start + step ** i, stop - step

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

In [ ]:
print(list(range(5)))
print(list(range(-5, 5)))
print(list(range(0, 15, 2)))
print(list(range(5, -5, -1)))
print(list(range(0)))
print(list(range(2, -2)))
print(type(range(10)))

Теперь предыдущая задача просто записывается следующим образом:

In [ ]:
a = ['A', 'B', 'C', 'D', 'E', 'F']
for i in range(len(a)): # 0, 1, 2, 3, 4, 5
    if (i + 1) % 2: print(a[i], '- нечетная буква')
    else: print(a[i], '- четная буква')

Нумерация последовательности `enumerate`

Еще одна полезная функция для использования индексов элементов - enumerate() DOCS. Синтаксис выглядит следующим образом:

enumerate(sequence, [start=0])
    := (start, x_0), (start + 1, x_1), ... (start + len(sequence) - 1, x_last)

На выходе функция выдает последовательность типа enumerate, элементы которой есть тьюплы (счетчик, элемент). Например:

In [ ]:
print(list(enumerate(a)))
print(list(enumerate(a, 1)))
type(enumerate(a))

Теперь предыдущая задача просто записывается следующим образом:

In [ ]:
a = ['A', 'B', 'C', 'D', 'E', 'F']
for i, letter in enumerate(a, 1): # (1, A), (2, B), (3, C), (4, D), (5, F)
    # множественное присваивание позволяет сразу класть элементы тьюпла в несколько переменных
    # i, letter = 1, A     и т.д.
    if i % 2:
        print(letter, '- нечетная буква')
    else: 
        print(letter, '- четная буква')

Агрегирование последовательностей `zip`

Теперь вернемся к общему случаю, когда итерирование происходит поэлементно. Впрочем, уже в предыдущем прмере с функцией enumerate мы итерировались именно по элементам, просто одновременно двух последовательностей: range(len(a)) и a. Проделать то же самое без enumerate можно было бы с помощью функции zipDOCS, применение которой сильно шире, чем замена enumerate. Её применение выглядит следующим образом:

zip(seq_1, seq_2, ...., seq_n)
    := (x_1_0, x_2_0, ..., x_n_0), (x_1_1, x_2_1, ..., x_n_1), ..., (x_1_m, x_2_m, ..., x_n_m)

где m это длина самой короткой последовательности среди seq_1, ..., seq_n. То есть в качестве аргументов мы подаем любое количество последовательностей, а на выходе получаем последовательность типа zip, элементы которой это тьюплы из i-х элементов каждой последовательности. Например:

In [ ]:
print(list(zip('abcdef', 'абвгдежзийклмнопрст')))
print(list(zip([42, 57, 121], ('answer', 'school', 'power'))))
print(list(zip('abcdef', 'абвгде', 'αβγδεζ')))
print(list(zip('12345')))
print(list(zip('')))

Теперь предыдущая задача просто записывается следующим образом:

In [ ]:
a = ['A', 'B', 'C', 'D', 'E', 'F']
for i, letter in zip(range(1, len(a)), a): # (1, A), (2, B), (3, C), (4, D), (5, F)
    if i % 2: print(letter, '- нечетная буква')
    else: print(letter, '- четная буква')

Порядок итерирования `reversed`, `sorted`

На том занятии мы познакомились с этими функциями, которые выдают последовательноти типа reverseiterator и обычный список соответственно, поэтому в качестве напоминания -- их тоже можно использовать для цикла:

In [ ]:
s = [1, 10, 23, -10, 3, 15, -9]

for el in reversed(s):
    print(el, end=' ')
print('\n-----')
for el in sorted(s):
    print(el, end=' ')
print('\n-----')
for el in reversed(sorted(s)):
    print(el, end=' ')

Генератор списка

Мы научились по-разному задавать итерации цикла и это прекрасно! Но прекраснее этого могут быть только генераторы списков DOCS. Это такой способ краткой записи цикла, который предназанчен (как правило) для создания списка. Конструкция выглядит так:

a = [строка_кода(x) for x in sequence]

Что является заменой такой записи:

a = []
for x in sequence:
    a.append(строка_кода(x))

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

In [ ]:
def forloop():
    a = []
    for x in range(10000):
        a.append(x)

def generatorloop():
    a = [x for x in range(10000)]

%timeit forloop()
%timeit generatorloop()

Несколько примеров применения генератора:

In [ ]:
print([x ** 2 for x in range(-4, 4)])
print([abs(x) for x in range(-4, 4)])
print([eng + '-' + rus for eng, rus in zip('abcdefg', 'абвгдеж')])

Как и обычные циклы, генераторы можно создавать вложенные:

In [ ]:
[(x, y) for x in range(1, 4) for y in range(4, 7)]
In [ ]:
[[x for x in range(3)] for y in range(4)]
In [ ]:
[[x for x in range(y * 3, y * 3 + 3)] for y in range(4)]
In [ ]:
[[x for x in range(y)] for y in range(2, 7)]
In [ ]:
matrix = [[1,2,3], [4,5,6], [7,8,9]]
[x for row in matrix for x in row]

Краткая запись `if`-конструкции

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

In [ ]:
inp = input()
x = 'yes' if inp == 'y' else 'no' # без else не работает
print(x)

Эту же конструкцию можно использовать при создании списка с помощью генератора:

In [ ]:
a = [x ** 2 if x < 0 else x ** 0.5 for x in range(-5, 5)]
print(a)

Причем в данном случае можно использовать конструкцию без else-части (чего нельзя сделать в предыдущем примере), но тогда if пишется справа:

In [ ]:
a = [x for x in range(10) if x % 2]
print(a)

Чтобы проще запомнить с какой стороны писать if в обоих случаях, попробуйте понять такой принцип:

a = [x |||| for x in последовательность if условие(х)]
       |||| 
a = [значение_true(х) if УСЛОВИЕ else значение_false(х) |||| for x in последовательность]
                                                        ||||

Мы сначала читаем то, что стоит справа от ||||, если дошли до конца, то кладем в список то, что стоит слева от ||||. То есть в первом случае, мы кладем значение в список только если условие выдает True. А во втором случае, что бы ни выдало условие - True или False - мы что-нибудь положим в список.

На самом деле первый случай это часть конструкции генератора, а второе это просто условное присваивание элемента внутри "строки кода" генератора. Общий вид генератора:

[строка_кода for ... ... ... for ... if ... ... ... if ...]
In [ ]:
[x * y for x in range(-2, 5) for y in range(5) if x < 3 if y > 1 if x != y]

В очередной раз можем перевести ввод чисел через пробел с клавиатуры в список чисел:

In [ ]:
[int(x) for x in input().split()]

Поэлементное применение функций `map`

Последняя на сегодня полезная функция mapDOCS. Вслед предыдущему примеру, можно переписать с помощью неё наш код для ввода списка:

In [ ]:
list(map(int, input().split()))

То есть синтаксис следующий:

map(функция, последовательность)
   := функция(x_1), функция(x_2), ..., функция(х_n)

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

In [ ]:
s = 0
for x in map(int, input().split()):
    s += x
print(s)
In [ ]:
n, m = map(int, input().split())
n, m