Все статьи по Python
17 февр. 2025 г. - 32 мин. чтения
Циклы

Циклы

Создание циклов. Условия внутри тела цикла. Обход строк

@ashtana

Штана Альберт Игоревич

Циклические конструкции

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

Зачем нужны циклы

Прикладные программы помогают управлять сотрудниками, финансами и могут развлекать. Несмотря на различия, они выполняют заложенные в них алгоритмы, которые похожи. Алгоритм — это последовательность действий, которая приводит к ожидаемому результату. Представим, что у нас есть книга, и мы хотим найти в ней конкретную фразу. Саму фразу мы помним, но не знаем, на какой она странице. Нам придется последовательно просматривать страницы до тех пор, пока не найдем нужную. Алгоритм включает логические проверки и перебор страниц. Количество страниц, которое придется посмотреть, заранее неизвестно. Но сам процесс просмотра повторяется одинаковым образом. Чтобы выполнять повторяющиеся действия, нужны циклы. Каждый повтор называется итерацией. Напишем функцию с простым циклом, который будет n раз выводить на экран строку 'Python':

def print_python(n):
    counter = 0
    while counter < n:
        print('Python')
        counter += 1

print_hello(2)
# => Python
# => Python

Теперь проанализируем пример функции с циклом, который выводит на экран числа от одного до числа-аргумента:

print_numbers(3)
# => 1
# => 2
# => 3

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

def print_numbers(last_number):
    # i — сокращение от index (порядковый номер)
    # используется по общему соглашению во множестве языков
    # как счетчик цикла
    i = 1
    while i <= last_number:
        print(i)
        i = i + 1
    print('finished!')

print_numbers(3)
# => 1
# => 2
# => 3
# => finished!

Цикл while

Цикл while состоит из трех элементов:

  • Ключевое слово while
  • Предикат — условие, которое указывается после while и вычисляется на каждой итерации
  • Блок кода — тело цикла

Каждое выполнение тела цикла называется итерацией. В примере выше print_numbers(3) вызвал три итерации, на каждой из которых была выведена на экран переменная i. Конструкция читается так: «делать то, что указано в теле цикла, пока истинно условие i <= last_number». Разберем работу этого кода для вызова print_numbers(3):

# Инициализируется i
i = 1

# Предикат возвращает true, поэтому выполняется тело цикла
while 1 <= 3
# print(1)
# i = 1 + 1

# Закончилось тело цикла, поэтому происходит возврат в начало
while 2 <= 3
# print(2)
# i = 2 + 1

# Закончилось тело цикла, поэтому происходит возврат в начало
while 3 <= 3
# print(3)
# i = 3 + 1

# Предикат возвращает false, поэтому выполнение переходит за цикл
while 4 <= 3

# print('finished!')
# На этом этапе i равен 4, но он нам уже не нужен
# Функция завершается

Завершение цикла

Процесс, который порождает цикл, должен остановиться. За это отвечает программист. Обычно задача сводится к введению переменной — счетчика цикла. Сначала он инициализируется — ему задается начальное значение. В нашем примере это строчка i = 1. Затем в условии цикла проверяется, не достиг ли счетчик своего предельного значения. Предельное значение в примере определяется аргументом функции. Если условие цикла ложно, то тело не выполняется и интерпретатор двигается дальше — работает с инструкциями после цикла. Если условие цикла истинно, то выполняется тело, в котором находится элемент остановки — изменение значения счетчика. Обычно его делают в конце тела, и это изменение — место, где нельзя обойтись без переменной. В примере выше за изменение отвечает строчка i = i + 1. На этом моменте новички много ошибаются. Например, можно забыть увеличить значение счетчика или неправильно проверить его в предикате. Это приведет к зацикливанию — цикл будет работать бесконечно, и программа никогда не остановится. В таком случае ее нужно завершить принудительно.

def print_numbers(last_number):
    i = 1
    # Этот цикл никогда не остановится
    # и будет печатать всегда одно значение
    while i <= last_number:
        print(i)
    print('finished!')

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

while True:
    # Что-то делаем

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

Синтаксический сахар

Конструкции типа x = x + 1 часто используются в Python, поэтому создатели языка добавили сокращенный вариант: x += 1. Они отличаются только способом записи. Интерпретатор превратит сокращенную конструкцию в развернутую. Такие сокращения называют синтаксическим сахаром, потому что они делают процесс написания кода немного проще и приятнее. Существуют сокращенные формы для всех арифметических операций и для конкатенации строк:

  • x = x + 1 → x += 1
  • x = x - 1 → x -= 1
  • x = x * 2 → x *= 2
  • x = x / 1 → x /= 1

Агрегация данных

Отдельный класс задач, который не обходится без циклов, называется агрегированием данных. К таким задачам относятся: поиск максимального или минимального значения, суммы, среднего арифметического. В этом случае результат зависит от всего набора данных. Чтобы рассчитать сумму, нужно сложить все числа, а чтобы вычислить максимальное, нужно их сравнить. С такими задачами хорошо знакомы бухгалтеры и маркетологи. Они работают с таблицами Microsoft Excel или Google Sheets. Разберем, как агрегация применяется к числам и строкам.

Числа

Допустим, нам нужно найти суммы набора чисел. Реализуем функцию, которая складывает числа в указанном диапазоне, включая границы. Диапазон — ряд чисел от конкретного начала до определенного конца. Например, диапазон [1, 10] включает целые числа от одного до десяти.

Пример:

sum_numbers_from_range(5, 7)  # 5 + 6 + 7 = 18
sum_numbers_from_range(1, 2)  # 1 + 2 = 3

# [1, 1] — диапазон с одинаковым началом и концом — тоже диапазон
# Он включает одно число — саму границу диапазона
sum_numbers_from_range(1, 1)      # 1
sum_numbers_from_range(100, 100)  # 100

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

def sum_numbers_from_range(start, finish):
    # Технически можно менять start
    # Но входные аргументы нужно оставлять в исходном значении
    # Это сделает код проще для анализа
    i = start
    sum = 0  # Инициализация суммы
    while i <= finish:  # Двигаемся до конца диапазона
        sum = sum + i   # Считаем сумму для каждого числа
        i = i + 1       # Переходим к следующему числу в диапазоне
    # Возвращаем получившийся результат
    return sum

Структура цикла здесь стандартная: есть счетчик, который инициализируется начальным значением диапазона, цикл с условием остановки при достижении конца диапазона и изменении значения счетчика в конце тела цикла. Количество итераций в таком цикле равно finish - start + 1. Для диапазона [5, 7] — это 7 - 5 + 1, то есть три итерации. Главное отличие от обычной обработки — логика вычисления результата. В задачах на агрегацию всегда есть переменная, которая хранит внутри себя результат работы цикла. В коде выше это sum. Она изменяется на каждой итерации цикла — прибавляется следующее число в диапазоне: sum = sum + i. Этот процесс выглядит так:

# Для вызова sum_numbers_from_range(2, 5)
sum = 0
sum = sum + 2  # 2
sum = sum + 3  # 5
sum = sum + 4  # 9
sum = sum + 5  # 14
# 14 – результат сложения чисел в диапазоне [2, 5]

У переменной sum есть начальное значение — с него начинается любая повторяющаяся операция. В примере выше — это 0. В математике есть понятие нейтральный элемент, и у каждой операции он свой. Операция с этим элементом не изменяет то значение, над которым работает. Например, при сложении любое число плюс ноль дает само число. При вычитании — то же самое. У конкатенации тоже есть нейтральный элемент — это пустая строка: '' + 'one' будет 'one'. Далее разберем, как агрегация применяется к строкам.

Строки

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

repeat('python', 3)  # 'pythonpythonpython'

Принцип работы этой функции — в цикле происходит «наращивание» строки указанное количество раз:

def repeat(text, times):
    # Нейтральный элемент для строк — пустая строка
    result = ''
    i = 1

    while i <= times:
        # Каждый раз добавляем строку к результату
        result = result + text
        i = i + 1

    return result

Распишем выполнение этого кода по шагам:

# Для вызова repeat('python', 3)
result = ''
result = result + 'python'  # python
result = result + 'python'  # pythonpython
result = result + 'python'  # pythonpythonpython

Обход строк

С помощью циклов не только обрабатывают числа, но работают и со строками. Например, можно получить конкретный символ по его индексу, а также формировать строки в циклах.

Получить символ по индексу

Ниже пример кода, который печатает каждую букву каждого слова на отдельной строке:

def print_name_by_symbol(name):
    i = 0
    # Такая проверка будет выполняться до конца строки,
    # включая последний символ. Его индекс len(name) - 1.
    while i < len(name):
        # Обращаемся к символу по индексу
        print(name[i])
        i += 1

name = 'Alex'
print_name_by_symbol(name)
# => 'A'
# => 'l'
# => 'e'
# => 'x'

Главное в этом коде — поставить правильное условие в while. Это можно сделать двумя способами: i < len(name) или i <= len(name) - 1 — они приведут к одному результату.

Как формировать строки в циклах

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

Один из алгоритмов выглядит так:

  • Строим новую строку;
  • Перебираем символы исходной строки в обратном порядке.
def reverse_string(string):
    index = len(string) - 1
    reversed_string = ''

    while index >= 0:
        current_char = string[index]
        reversed_string = reversed_string + current_char
        # То же самое через интерполяцию
        # reversed_string = f'{reversed_string}{current_char}'
        index = index - 1

    return reversed_string

reverse_string('Game Of Thrones')  # 'senorhT fO emaG'
# Проверка нейтрального элемента
reverse_string('')  # ''

Разберем функцию построчно:

  • index = len(string) - 1 — записываем в новую переменную индекс последнего символа строки (индексы начинаются с нуля);
  • reversed_string = '' — инициализируем строку, куда будем записывать результат;
  • while index >= 0: — условие: повторяем тело цикла, пока текущий индекс не дошел до 0 — до первого символа;
  • current_char = string[index] — берем из строки символ по текущему индексу;
  • reversed_string = reversed_string + current_char — записываем в строку-результат новое значение: текущая строка-результат + новый символ;
  • index = index - 1 — обновляем счетчик;
  • return reversed_string — когда цикл завершился, возвращаем строку-результат.

Советуем скопировать эту функцию и поэкспериментировать с ней. Работая со строками, программисты часто допускают ошибку — выходят за границы строки. Если неправильно подобрать начальное значение счетчика или допустить ошибку в предикате цикла, функция может обращаться к несуществующему символу. Особенно часто забывают, что индекс последнего элемента всегда меньше на единицу размера строки. В строках начальный индекс равен 0, значит, индекс последнего элемента — len(str) - 1.

Условия внутри цикла

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

count_chars('Fear cuts deeper than swords.', 'e')  # 4
# Если вы ничего не нашли, то результат — 0 совпадений
count_chars('Sansa', 'y')  # 0

Перед тем как посмотреть содержимое функции, подумайте: Является ли эта операция агрегацией? Какой будет проверка на вхождение символа?

def count_chars(string, char):
    index = 0
    count = 0
    while index < len(string):
        if string[index] == char:
            # Считаем только подходящие символы
            count = count + 1
        # Счетчик увеличивается в любом случае
        index = index + 1
    return count

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

Пограничные случаи

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

А теперь представим, что ей передали такие варианты длины:

  • 0;
  • Отрицательное число;
  • Число, которое превышает реальный размер строки;

Функция my_substr() не рассчитана на такие варианты. Код будет запускаться в разных ситуациях, с разными комбинациями условий и данных. Нельзя быть уверенным, что аргументы всегда будут корректными, поэтому нужно учитывать все случаи. Ошибки в пограничных случаях — частая причина логических ошибок в программах. Программисты всегда забывают что-нибудь учесть. Такие ошибки часто проявляются не сразу и могут долго не приводить к видимым проблемам. Программа продолжает работать, но в какой-то момент обнаруживается, что в результатах есть ошибки. Часто причина в динамической типизации Python. Вы научитесь справляться с такими ошибками с опытом.

Представим расширенную функцию my_substr(). Она принимает три аргумента: строку, индекс и длину извлекаемой подстроки. Функция возвращает подстроку указанной длины, начиная с указанного индекса. Примеры вызова:

string = 'If I look back I am lost'
print(my_substr(string, 0, 1))  # => 'I'
print(my_substr(string, 3, 6))  # => 'I look'

Какие пограничные случаи стоит учитывать:

  • Отрицательная длина извлекаемой подстроки;
  • Отрицательный заданный индекс;
  • Заданный индекс выходит за границу всей строки;
  • Длина подстроки в сумме с заданным индексом выходит за границу всей строки.

Когда функция реализуется, каждый пограничный случай будет отдельным блоком кода. Скорее всего, он будет реализовываться с помощью if. Чтобы написать функцию my_substr() и защититься от этих случаев, стоит реализовать отдельную функцию, которая будет проверять аргументы на корректность.

Возврат из циклов

Работа с циклами обычно сводится к двум сценариям:

  1. Агрегация. Накопление результата во время итераций и работа с ним после завершения цикла. Переворот строки относится к такому варианту
  2. Выполнение цикла до достижения необходимого результата и выход. Например, задача поиска простых чисел — которые делятся без остатка только на себя и на единицу.

Рассмотрим алгоритм проверки простоты числа. Будем делить искомое число x на все числа из диапазона от двух до x - 1 и смотреть остаток. Если в этом диапазоне не найден делитель, который делит число x без остатка, значит, перед нами простое число. В этом случае достаточно проверять числа не до x - 1, а до половины числа. Например, 11 не делится на 2, 3, 4, 5. Но и дальше не будет делиться на числа больше своей половины. Значит, можно оптимизировать алгоритм и проверять деление только до x / 2:

def is_prime(number):
    if number < 2:
        return False

    divider = 2

    while divider <= number / 2:
        if number % divider == 0:
            return False

        divider += 1

    return True

print(is_prime(1))  # => False
print(is_prime(2))  # => True
print(is_prime(3))  # => True
print(is_prime(4))  # => False

Если быть честными, то для решения задачи хватит проверки чисел до значения квадратного корня number, но в нашем случае важно сосредоточиться на понимании работы с условиями внутри цикла. Представим, что по алгоритму последовательного деления на числа до x / 2 нашлось одно, которое делит без остатка. Значит, переданный аргумент — не простое число, и дальнейшие вычисления не имеют смысла. В этом месте стоит возврат False. Если цикл отработал целиком, а число, которое делит без остатка, не нашлось, значит, число — простое.

Цикл for

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

sum = 0
i = 0
while i < 10:
    sum += i
    i += 1
print(sum) # => 45

Сначала мы устанавливаем начальную сумму 0. Далее запускается цикл, в котором переменная i начинает принимать значения начиная с 0 и доходя до 10. На каждом шаге мы прибавляем значение i к нашей сумме и увеличиваем i на 1. Как только i становится равным 10, цикл заканчивается и программа выдаёт нам сумму всех чисел от 0 до 9 равную 45. Такой код мы можем переписать на цикл for:

sum = 0
for i in range(10):
    sum += i
print(sum) # => 45

Первый пример использует while, который продолжает работать пока i < 10. Второй использует for и выполняет итерацию от 0 до 9 с помощью функции range(). Оба выполняют одно и то же: складывают числа от 0 до 9 в переменную sum, но используют разные способы выполнения итераций.

Функция range

Функция range в Python является встроенной функцией, которая создает последовательность чисел внутри определенного диапазона. Подобные функции, которые создают, еще говорят "генерируют"(называют генераторными функциями), можно использовать в цикле for для контроля количества итераций.

У range есть несколько вариантов использования:

  • range(stop) создает последовательность от 0 до stop - 1;
  • range(start, stop) создает последовательность от start до stop - 1;
  • range(start, stop, step) создает последовательность из чисел от start до stop - 1, с шагом step.

Пример с одним конечным значением мы рассмотрели выше. Рассмотрим другой - распечатаем на экран числа от 1 до 3:

for i in range(1, 4):
    print(i)

# => 1
# => 2
# => 3

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

for i in range(3, 0, -1):
    print(i)

# => 3
# => 2
# => 1

На примерах выше мы видим, что итерация завершается до конечного значения.

Если нужно просто повторить действие несколько раз, и переменная не нужна, то её можно опустить. Для этого имя переменной заменяют на символ _:

for _ in range(1, 4):
    print('Python!')

# => Python!
# => Python!
# => Python!

Цикл for и строки

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

text = 'code'
for symbol in text:
    print(symbol)

# => c
# => o
# => d
# => e

В коде выше for проходит по каждому символу в строке, записывает его в переменную symbol и вызывает внутренний блок кода, где эта переменная используется. Имя этой переменной может быть любым. Общая структура цикла for выглядит так: for <переменная> in <коллекция>. Посмотрим, как реализовать функцию переворота строки через цикл for:

def reverse_string(text):
    # Начальное значение
    result = ''
    # char - переменная, в которую записывается текущий символ
    for char in text:
        # Соединяем в обратном порядке
        result = char + result
    # Цикл заканчивается, когда пройдена вся строка
    return result


result = reverse_string('go!')
print(result) # => !og

Теперь посчитаем количество упоминаний символа в строке без учета регистра:

# text - произвольный текст
# char - символ, который нужно учитывать
def chars_count(text, char):
    # Так как ищем сумму, то начальное значение — 0
    result = 0
    for current_char in text:
        # Приводим все к нижнему регистру,
        # чтобы не зависеть от текущего регистра
        if current_char.lower() == char.lower():
            result += 1
    return result


chars_count('python!', 'o')  # 1
chars_count('pYthon!', 'y')  # 1
chars_count('pYthon!', 'Y')  # 1
chars_count('python!', 'a')  # 0

Главное преимущество for в том, что он короче и проще, когда известно сколько итераций будет. Ещё он понятнее и легче читается, например со строками. В Python он ещё и управляет итерациями сам, что делает его удобнее в некоторых случаях.

Попробуйте сами запустить код в окне ниже с интерпретатором Python и повторите примеры из статьи чтобы самим увидеть и понять как всё это работает. Для этого в ячейке с кодом нажмите клавиши на клавиатуре Shift+Enter или запустите код через кнопку Run по значку ▶.