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

Например, когда нам нужно выйти из вложенных циклов:

for a in list_a:
    for b in list_b:
        if condition(a,b):
            break

break может помочь выйти только из внутреннего цикла. Можем ли мы напрямую выйти из двух вложенных циклов одновременно? Есть ли в Python какие-то встроенные ключевые слова или приемы для этого?

К сожалению, встроенная поддержка этой операции отсутствует.

В Python нет такой возможности, но она есть в других языках, например, PHP:

foreach ($a_list as $a)
{
    foreach ($b_list as $b)
    {
        if (condition($a, $b))
        {
            break 2; //break out of 2 loops
        }
    }
}

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

Поскольку Python очень гибкий, у нас есть много других способов получить тот же результат без встроенной поддержки.

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

1. Добавьте флаг

Определим переменную и используем ее в качестве флага. Рассмотрим простой пример:

# add a flag variable
break_out_flag = False
for i in range(5):
    for j in range(5):
        if j == 2 and i == 0:
            break_out_flag = True
            break
    if break_out_flag:
        break

Как показано выше, переменная break_out_flag - это флаг, сообщающий программе, когда ей следует выйти из внешнего цикла.

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

Давайте рассмотрим другие варианты.

2. Бросить исключение

Если мы не можем использовать ключевое слово break, почему бы не реализовать выход из циклов другим способом? С помощью методов обработки исключений в Python мы можем выйти из вложенных циклов следующим образом:

# raise an exception
try:
    for i in range(5):
        for j in range(5):
            if j == 2 and i == 0:
                raise StopIteration
except StopIteration:
    pass

3. Проверьте то же условие еще раз

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

# check the same condition again
for i in range(5):
    for j in range(5):
        if j == 2 and i == 0:
            break
    if j == 2 and i == 0:
        break

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

4. Используйте синтаксис For-Else

В Python есть специальный синтаксис: "for-else". Он не популярен, а кто-то даже никогда его не использовал. Потому что все привыкли использовать "else" после "if".

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

# use the for-else syntax
for i in range(5):
    for j in range(5):
        if j == 2 and i == 0:
            break
    else:  # only execute when it's no break in the inner loop
        continue
    break

Приведенный выше код использует преимущества техники "for-else", поскольку код под оператором else будет выполняться только тогда, когда внутренний цикл завершится без break.

5. Поместите циклы в функцию

Если мы поместим вложенные циклы в функцию, проблема break становится простой. Потому что мы можем использовать ключевое слово return вместо break.

# make it as a function
def check_sth():
    for i in range(5):
        for j in range(5):
            if j == 2 and i == 0:
                return
check_sth() # Run the function when needed

Как показано выше, это решение выглядит более элегантно. Здесь нет переменных флагов, синтаксиса "try-except" или "for-else" и ненужной проверки условий.

Кроме того, "Turn Predicate Loops into Predicate Functions" - это хорошая практика написания кода, введенная командой компилятора LLVM.

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


def out_func():
    # do something
    def check_sth():
        for i in range(5):
            for j in range(5):
                if j == 2 and i == 0:
                    return
    # do something
    check_sth()  # Run the function when needed
    # do something

Вывод: Избегайте вложенных циклов

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

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

# Avoid nested loops
import itertools
for i, j in itertools.product(range(5), range(5)):
    if j == 2 and i == 0:
        break

Как показано выше, наш предыдущий пример может избежать вложенных циклов с помощью функции itertools.product. Это простой способ получить декартово произведение входных итераций.

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

Спасибо за прочтение! Какой из способов вы считаете самым полезным? Пишите в комментариях!

Еще больше примеров использования Python и Machine Learning в современных сервисах можно посмотреть в моем телеграм канале. Я пишу про разработку, ML, стартапы и релокацию в UK для IT специалистов.

Комментарии (18)


  1. rubinstein
    00.00.0000 00:00
    +5

    А где goto?


    1. dndred
      00.00.0000 00:00
      +1

      В Python нет goto


      1. Yuri0128
        00.00.0000 00:00

        Ну, как-бы реализовать его можно, и не так чтобы некрасиво. Просто зачем?


  1. Sild
    00.00.0000 00:00
    +1

    Почему работает 3ий вариант, если область видимости j должна бы быть ограничена вложенным циклом?


    1. fireSparrow
      00.00.0000 00:00
      +2

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

      В питоне это не так, здесь цикл не порождает отдельной области видимости.


  1. adeshere
    00.00.0000 00:00
    +3

    Я на Питоне не пишу, но интересуюсь концепциями, и мне удивительно, что в языке нет именованных циклов. У меня в 100-летнем фортране пишется что-то типа "exit Loop_A", где Loop_A - это метка (название) цикла. А вот конструкции типа "break 2" (с номерами ) ввиду очевидной некошерности стали нерекомендованными

    еще лет 20 назад

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

    Как не надо писать программы
       CALL CHECK(A, *10, *20)   
    ...
    10 
    ...
    20 
    ...   
       SUBROUTINE CHECK(X, *, *)   
    ...
    50   IF (X) 60, 70, 80
    60   RETURN 
    70   RETURN 1
    80   RETURN 2   
         END

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

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

    Если я правильно понял примеры в статье, то создатели Питона (а также PHP) посчитали, что аналогичные конструкции (именованные циклы) в этих языках не нужны. Это потому, что они не востребованы? Или они как-то противоречат философии языка? Я в курсе, что "Должен быть один - и желательно только один - очевидный способ сделать это". Но обертка в функцию, а тем более вариант с флагами как-то не кажутся наиболее естественным выходом...


    1. Jury_78
      00.00.0000 00:00
      +1

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


    1. Andrey_Solomatin
      00.00.0000 00:00
      +3

      Официальный ответ на вопрос про метки:
      https://mail.python.org/pipermail/python-3000/2007-July/008663.html


  1. LaRN
    00.00.0000 00:00

    Тут наверное можно ещё yield упомянуть.

    Он тоже останавливает итерации цикла.


    1. Gadd
      00.00.0000 00:00
      +1

      Не останавливает, а ставит на паузу.

      def ttst():
          for i in range(5):
              for j in range(5):
                  print(i, j)
                  if j == 2 and i == 0:
                      yield
      
      t = ttst()
      
      >>> next(t)
      0 0
      0 1
      0 2
      >>> next(t)
      0 3
      0 4
      1 0
      1 1
      1 2
      1 3
      1 4
      ...
      4 4
      Traceback (most recent call last):
      ...
      StopIteration


  1. lxsmkv
    00.00.0000 00:00
    +1

    Не вижу смысла рассматривать подходы в отрыве от практической задачи.

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

    board = [ 
    ["x", "x", "x", "x", "x"],
    ["x", "x", "x", "x", "x"],
    ["x", "x", "x", "x", "x"],
    ["x", "x", "x", "o", "x"],
    ["x", "x", "x", "x", "x"]]
    
    for row in board:
        for col in row:
            if col == "o":
                break
        else:  
            continue
        break
    
    print(row)
    
    print(list(filter(lambda row: "o" in row, board))[0])


    1. Andrey_Solomatin
      00.00.0000 00:00

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

      for row in board:
          if "o" in row:
            break
          else:
              continue
          break

      А если пойти дальше, то и фильтровать можно сразу

      print(next(row for row in board if "o" in row))


  1. randomsimplenumber
    00.00.0000 00:00
    +1

    Можно вместо for использовать while.

    i=0
    j=0
    found=False
    while not found and i<5:
      while not found and j<5:
        #Do something
        if something ():
          found=True 
        j=j+1
      i=i+1


  1. Azya
    00.00.0000 00:00
    +2

    Элегантное прерывание цикла - это оксюморон.


  1. deadmoroz14
    00.00.0000 00:00
    +17

    Хабы:  Python, Программирование, Алгоритмы, Машинное обучение, Искусственный интеллект

    Нет в статье ничего ни про Машинное обучение, ни про ИИ. Это уже вторая такая статья от вас. Не засоряйте людям ленту


  1. barsik_unlimited
    00.00.0000 00:00

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

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

    П. С. Я уже молчу про чтение чужого кода.


  1. vitalijspagin
    00.00.0000 00:00

    Рекурсия тоже как вариант


  1. xilot
    00.00.0000 00:00

    Про for-else знал, но никогда не использовал)))