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

Циклы в программировании

Компьютер — максимально тупой аппарат, но при этом максимально быстрый по сравнению с человеком. По факту компьютер очень быстро перебирает варианты и ищет подходящий. В этом его сила. И сила эта реализуется через циклы — специальный инструмент, который позволяет пробегаться по значениям и что-то с ними делать.

Есть разные виды циклов, но чаще всего применяют циклы while и for. Я пользуюсь вторым циклом, потому что он для меня проще, полезнее и с куда меньшей вероятностью может зависнуть. Потому что цикл for проходится по выданному ему объекту, а когда объект заканчивается, то и цикл завершается. А цикл while выполняется до тех пор, пока не сработает какое-то условие. И так как не факт, что условие выполнится, то программа забивает всю память компьютера, и он зависает.

Так что буду рассказывать про цикл for. В этой статье разбираем концептуально и синтаксис, позже будут статьи, в которых уже будем применять циклы на рабочих задачах. Так что цель — разобраться, как оно работает, а уже практика будет позже.

Объекты для обработки циклом

В цикл можно подавать итерируемые объекты, по-русски — перебираемые. Итерируемые объекты состоят из элементов, которые можно перебирать по одному. Например, это списки, строки, словари и кортежи.

Список состоит из каких-то элементов вроде чисел или слов или других списков. Его содержимое можно перебирать. Строка — это набор букв, поэтому строку можно перебирать по буквам. Про словари мы будем говорить дальше в отдельном материале, но по сути это как бы два спаренных списка. Кортежи — это специальные списки, которые нельзя изменить после создания. С ними работать не будем, кортежи для депутатов, а мы люди простые.

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

Цикл for получает список, перебирает каждый его элемент и выполняет с каждым элементом то, что мы его попросим. Когда элементы кончаются, то и цикл завершает работу. Один проход цикла называется итерацией. Если подаём список из пяти элементов, то будет пять итераций, если мы их не прервём раньше.

Чтобы визуализировать работу циклов, я иногда представляю себе коробку, в которой что-то лежит. Циклом я залезаю коробку и вытаскиваю её содержимое по одной штуке за раз. Вытащил — что-то сделал, вытащил следующее — что-то сделал. При этом в коробке могут быть и другие коробки. Тогда мне придётся залезать ещё и в эти коробки, чтобы вытащить из них каждую штуку. Это будет цикл в цикле, сон во сне, иголка в зайце, пейн ин эсс.

Синтаксис цикла for

Посмотрим на примере простого объекта — списка чисел. Именно список чисел, а не просто отдельное число, потому что число нельзя перебирать, а список — можно.

На языке Питона это будет выглядеть так, слева в Код Блоке создал список чисел от 0 до 5 и подал в Питон-нод:

for chislo in spisok_chisel:
    # тут что-то делаем
    # с элементами списка

После двоеточия делаем абзац, дальше — список команд, которые будут выполняться внутри цикла. Чтобы компьютер понимал, какие команды надо выполнять внутри данного цикла, то все последующие строки внутри цикла мы пишем с отступом. Отступы можно делать либо клавишей табуляции, если вы психически здоровый человек, либо с помощью четырёх пробелов, если вы больной ублюдок. Если один раз нажать Таб вам сложнее, чем четыре раза тыкать пробел, то вам надо к доктору.

Это довольно важно, потому что код, в котором где-то табы, где-то пробелы просто не запустится. Получите ошибку, показал её на скриншоте ниже. Я вам покажу, как проверять код, как быстро поменять пробелы на табы или обратно, но это будет позже, когда дойдём до работы с VS Code.

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

По-русски запись цикла можно прочитать так: для каждого значения в списке итерируемом объекте.

Здесь chislo — это переменная, которую объявил, чтобы в неё класть элементы списка. По сути я в эту переменную на каждой итерации цикла по очереди копирую значения из списка. Это не сам элемент списка, дальше увидим, почему.

Переменная нужна, потому что в программировании все данные должны храниться в памяти компьютера. Чтобы выделить место в памяти под что-то, мы создаём переменную. Список чисел уже создали, он уже занимает какое-то место в памяти.

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

for и in — это ключевые слова, с помощью for объявили цикл, с помощью in указали, в каком итерируемом объекте будем брать элементы. Ключевые слова нельзя использовать как переменные, мы не можем назвать переменную for, программа нас не поймёт.

spisok_chisel — наш список с числам, по которому проходимся циклом, по которому итерируемся.

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

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

Дело в том, что внутри цикла мы меняли значение переменной chislo, но ведь это совершенно отдельная переменная. Мы просто последовательно копировали в неё значения из списка, добавляли единицу, потом в эту же переменную копировали следующее значение, добавляли единицу и так далее.

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

Запись новых значений в список

Чтобы записать результат преобразований в цикле, создадим новый список, куда будем складывать новые числа. Для этого нужно объявить новую переменную и записать в неё пустой список. Делается это очень просто: noviy_spisok = []. Вот эти квадратные скобки означают список, а так как внутри скобок ничего нет, то список будет пустым в момент создания.

Чтобы добавить в список элемент, применяют метод append. Пока просто запомните, разбираться с этим будем в отдельной статье. Запись выглядит так: noviy_spisok.append(chislo)

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

Когда пишете имя переменной со списком, а потом ставите точку и пишете букву a на латинице, то редактор кода в Динамо сразу предлагает список методов, первый в списке — нужный нам метод append. Нажмите энтер и писать его целиком не придётся. Только заглавную А замените на строчную. Зачем они сделали с заглавных букв — непонятно.

Про методы будем говорить в другой статье.

Где создавать пустой список

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

Если создадим список перед циклом, то всё будет работать правильно. Алгоритм получается следующий:

  1. У нас есть исходный список и наш новый пустой. Они оба уже существуют. То есть нам есть, откуда брать и куда складывать элементы списка.
  2. Мы заходим циклом в исходный список. Берём первый элемент списка.
  3. Меняем элемент списка. Кладём его в новый пустой. Теперь новый список не пустой, в нём один объект.
  4. Запускается следующая итерация цикла: заходим во второй элемент исходного списка, меняем, складываем в новый список.
  5. Цикл завершается, когда мы зайдём в каждый элемент, изменим его и положим в новый список.
  6. На выходе у нас будет неизменный исходный список и новый список с изменёнными значениями

Если создадим список внутри цикла, то записать все новые значения в него не получится. Потому что алгоритм изменится:

  1. У нас есть исходный список. Новый пока не существует. Нам есть, откуда взять элементы, но положить пока некуда.
  2. Заходим циклом в исходный список. Создаём новый список. Берём первый элемент. Меняем его и кладём в новый список. Первая итерация завершена.
  3. Теперь мы циклом заходим во второй элемент. При этом мы опять создаём пустой список, старый при этом нигде не сохранился, так как мы в ту же переменную засунули новый пустой список. И снова кладём в него изменённый элемент.
  4. И так будет в каждой итерации цикла.
  5. Когда цикл завершится, мы получим на выходе неизменный исходный список и новый список с последним элементом исходного списка.

Всё потому, что мы по факту в каждой итерации заново генерировали пустой список. Мы не сохраняли результат всех действий после выполнения каждой итерации. Если в моём примере выше взять и вывести в OUT не исходный список чисел, а саму переменную chislo, то мы получим значение последнего элемента списка плюс один.

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

Рабочий код

Итак, код будет выглядеть вот так:

spisok_chisel = IN[0] # исходный список

noviy_spisok = [] # список для записи значений

for chislo in spisok_chisel:
    chislo = chislo + 1
    noviy_spisok.append(chislo)

OUT = noviy_spisok

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

В общих чертах вот так работает цикл.

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

Склеивание нескольких списков в цикле

Супер полезная штука, когда нужно что-то сделать с двумя или более списками. Как правило, это списки равной длины, то есть у них одинаковое количество элементов.

Для примера возьмём два списка чисел. Задача — перемножить каждый элемент списка с его «соседом» из второго списка. То есть первый элемент первого списка умножить на первый элемент второго списка, далее второй из первого на второй из второго и так далее.

Для этого мы воспользуемся функцией zip. Мы как бы склеим два списка и в момент обработки циклом за одну итерацию мы будем брать последовательно элементы из всех склеенных списков. То есть функция как бы ставит две коробки с предметами рядом и за одну итерацию вытаскиваем элементы сразу из двух коробок.

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

Код будет выглядеть вот так:

spisok_chisel_1 = IN[0]
spisok_chisel_2 = IN[1]

umnozhenie = []

for num1, num2 in zip(spisok_chisel_1, spisok_chisel_2):
    resultat = num1 * num2
    umnozhenie.append(resultat)

OUT = umnozhenie

Что тут происходит. Создали два списка чисел в Динамо, подали в Питон-скрипт, присвоили две переменные spisok_chisel_1 и spisok_chisel_2. Дальше создал новый пустой список для записи результатов умножения.

И теперь цикл. В нём я пишу ключевое слово for, но затем создаю две переменные через запятую, потому что у меня два списка, и из каждого надо взять по одному элементу. Их надо записать в память компьютера, поэтому нужны две переменные. Назвал их num1 и num2.

После этого склеиваю списки функцией zip. Как она работает под капотом, неважно, главное, что делает то, что нужно.

Внутри цикла создаю переменную resultat, чтобы в ней перемножить два числа. И добавляю этот результат в новый список. Вывожу его на OUT и получаю нужный результат.

Функция zip тут очень облегчает работу. Без неё я бы делал цикл в цикле, брал бы индексы элементов, чтобы найти соответствующий, в общем, это была бы морока. А тут всё так просто происходит. Про работу с индексами списков тоже поговорим, то в статье о списках.

Вот так на базовом уровне работают циклы. В реальной работе вы будете итерироваться по разным спискам: и элементы Ревита, и числа, и текст, и вложенные списки. Сперва будет тяжеловато, особенно когда речь про вложенные списки, но со временем придёт понимание. Раскладывайте в уме списки на «коробки», можете в голове крутить итерации и писать результат на бумажку, а потом сравнивайте с тем, что выдаёт Питон, ищите ошибки. Это всё довольно абстрактно, поэтому понимание приходит не сразу.