Как сделать «двойной break», то есть выйти из вложенного цикла, в Python?

nested_loops

Условие:

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

Решение достаточно очевидное, но возникает вопрос:

Если бы мы программировали, например, на Java, то мы могли бы воспользоваться механизмом меток:

Однако в Python такого механизма нет. Требуется предложить наиболее удобное в использовании и читаемое решение.

Возможные варианты ответа

  • Поместить цикл в тело функции, а затем сделать return из неё:
    Почему это плохая идея: разумеется, сама задача в условии — лишь абстрактный пример. В жизни циклов может быть гораздо больше, и создавать по функции для каждого из них как-то неестественно, не так ли?
  • Выбросить исключение и поймать его снаружи цикла:
    Почему это плохая идея: здесь мы используем механизм исключений как особую форму goto, но ведь на самом деле ничего исключительного в коде не произошло — это обычная ситуация. Как минимум, причины такого злоупотребления этим механизмом будут непонятны другим программистам.
  • Можно создать булевую переменную, которая будет хранить информацию о том, нужно ли выходить из внешнего цикла на данной итерации:
    Почему это плохая идея: из всех перечисленных выше идей эта, пожалуй, лучшая. Тем не менее, это весьма низкоуровневый подход, и в языке Python есть возможность реализовать задуманное гораздо лучше.
  • Использовать вместо двух циклов for один while:
    Почему это плохая идея: вам не кажется, что такой код читается хуже всех предложенных вариантов?

Решение на пятёрку

Давайте ещё раз внимательно прочитаем условие:

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

Где там вообще хоть слово про двойной цикл или про перебор двух индексов? Нам нужно перебирать пары. Значит, по идее, мы должны написать что-то вроде этого:

Отлично, так мы будем перебирать пары. Но как нам добиться именно такой формы записи? Всё очень просто, нужно создать генератор. Делается это следующим образом:

«Как это работает?» — спросите вы. Всё просто. При вызове unique_pairs(int) код в теле функции не вычисляется. Вместо этого будет возвращён объект генератора. Каждый вызов метода next() этого генератора (что неявно происходит при каждой итерации цикла for) код в его теле будет выполняться до тех пор, пока не будет встречено ключевое слово yield. После чего выполнение будет приостановлено, а метод вернёт указанный объект (здесь yield действует подобно return). При следующем вызове функция начнёт выполняться не с начала, а с того места, на котором остановилась в прошлый раз. При окончании перебора будет выброшено исключение StopIteration.

Итак, самый true pythonic way в решении этой задачи:

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

Что же, для данной задачи это действительно более pythonic решение. Хочется отметить, что целью статьи было скорее познакомить новичков с механизмом генераторов, нежели действительно решить проблему, заявленную в первом абзаце 😉


Свои варианты предлагайте в комментариях!

Разбор взять из статьи «Breaking out of two loops»Пётр Соковых, транслятор двоичного кода в русский язык