вторник, 28 февраля 2017 г.

Python. Как перейти между внутренними циклами глубокой вложенности.

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


Немного истории

На заре Python3 была предпринята попытка ввести в состав языка конструкции позволяющие реализовать поведение подобное в других языках программирования. Этой попыткой был PEP 3136 (http://bit.ly/2lve7Ny), хорошо описывающий проблематику и предлагающий разные варианты решений, такие как ввод нового ключевого слова label или использование уже существующего as для именования циклических конструкций. Однако этот PEP был отвергнут создателем языка Гвидо. Он достаточно развернуто и аргументированно изложил причины отказа, и если кратко передать их суть, то количество затрат на введение этого в язык не соответствует получаемой выгоде. Ну или по простому - это того не стоит.

Проблемное место

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


Элегантное решение

Поиск привел меня на ряд из ответов на StackOverflow, объединенных общим подходом. Я разберу один из них (http://bit.ly/2lQfzMJ):
Мы определили функцию, обернутую декоратором, тело которой можно условно разделить на две части. Первая состоит из определения необходимых внутренних классов: основного Unblock и исключения Escape. Вторая в yield экземпляра Unblock обернутого в обработку исключения Escape.

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


Итак, как же это работает?

Что бы понять как работает данный код нужно иметь представления о том как работает в Python контекстный менеджер (http://bit.ly/2mHo0aT). После этого обратить внимание на принцип работы декоратора contextmanager (http://bit.ly/2m40asK).

Вызов функции escapable в качестве контекстного менеджера приостановит выполнение ее тела сразу после передачи yield'ом экземпляра класса Unblock, который свяжется с именем escape. Обращу внимание на то что функция не завершила свое исполнение, а лишь приостановилась и передала управление далее. В нужный момент происходит вызов метода escape() который возбуждает исключение. Стоит обратить внимание и понимать, что исключение порождается в экземпляре класса Unblock и, поднимаясь по иерархии объектов, будет обработано внутри функции escapable, тем самым вновь, передав ей управление исполнением. Так как никаких инструкций более в функции не предусмотрено, то произойдет выход из нее и, как следствие, выход из блока with,  c  последующей итерацией цикла for первого уровня.


13 комментариев:

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

    ОтветитьУдалить
  2. Обратите внимание на PEP 3136 который я упомянул вначале, прочитайте его весь, там и об этом говорилось.
    Если циклов еще больше или логика сложнее, то слишком много будет конструкций try...except, и этот код будет выглядеть страшным.
    К тому же хорошей практикой считается оборачивать в try...except только блоки кода в котором может возбуждаться исключение, а не большие блоки логики.
    В каких то случаях лучше разворачивать циклы.

    ОтветитьУдалить
    Ответы
    1. Если в программе много вложенных циклов и требуется подниматься на несколько уровней вверх, с пропуском итераций, то это явный признак неудачной архитектуры и править нужно именно логику, а не придумывать костыльные решения.
      Да я согласен, что try...except при таком использовании делает код менее читаемым, но использование контекстных менеджеров вводит в намного большее заблуждение, т.к. они абсолютно не для этого предназначены. Сами посудите, чтобы понять что произошел выход в вашем примере нужно:
      1. Понять что функция escape() делает выход именно из блока with, что абсолютно неочевидно, без захода в функцию
      2. Чтобы понять на какую строчку перейдет исполнение кода, нужно грубо говоря посмотреть на каком отступе начинается with, перемотать блок до его завершения (а если он очень длинный? много циклов например)

      В таком случае с try...except проще, сразу видно что в данном месте бросилось исключение, сразу видим место, где оно обрабатывается.

      Но тем не менее, это все в первую очередь проблемы архитектуры скрипта, и править нужно именно её)

      Удалить
    2. Этот комментарий был удален администратором блога.

      Удалить
    3. Этот комментарий был удален автором.

      Удалить
    4. Этот комментарий был удален администратором блога.

      Удалить
    5. Этот комментарий был удален автором.

      Удалить
    6. Этот комментарий был удален администратором блога.

      Удалить
    7. Этот комментарий был удален автором.

      Удалить
    8. Вы так и не ответили на вопрос

      Удалить
    9. Этот комментарий был удален автором.

      Удалить
  3. может есть смысл разбить на несколько функций?

    def q(x):
    for y in range(10):
    # some another actions

    for z in range(10):
    # yet another actions
    if x > 5 and y > 5 and not z % 2:
    # iter next from first for cycle
    return

    for x in range(10):
    # some actions
    q(x)

    ОтветитьУдалить
  4. А чем не устраивает подход с комбинацией continue - break?
    for i in range(0, 10):
    ----# fist cycle action
    ----for j in range (0,10):
    --------# second cycle action
    --------for k in range(0,10):
    ------------# third cycle action
    ------------if i > 5 and j > 5 and not k % 2:
    ----------------break # break third cycle
    --------else:
    ------------continue
    --------break # break second cycle if break third cycle, begin fist cycle nex iter
    И никакой магии..

    ОтветитьУдалить