Программирование на Python

Программное возбуждение исключений. Коллективная обработка исключений

Разбить на страницы
Показывать лекцию целиком

Когда мы работаем с собственным классом и спроектированным для него классом исключений, то, понятно, исключение может появиться только в результате выполнения кода, написанного программистом. Обнаружить ситуацию, приводящую к некорректной работе класса и его объектов, создать в этом случае объект исключение, передав ему соответствующие аргументы, - все это выполняется обычными программными средствами, ведь исключение это такой же объект, как и другие объекты, с которыми работает программист. Но для того чтобы реально возникла исключительная ситуация, прерывающая выполнение программного кода, необходим в этом случае специальный оператор, который "возбуждает" исключение. Иногда говорят, что оператор "поднимает" или " выбрасывает" исключение. Таким оператором в языке Python является оператор raise. Синтаксис оператора имеет вид:

raise [<expression1> [from <expression2>] ]

Рассмотрим семантику оператора:

  • Если выражение отсутствует и задано только ключевое слово raise, то возбуждается последнее исключение, активное в области действия оператора raise. Если активного исключения нет, то возбуждается исключение класса RuntimeError.
  • Выражение1 должно быть именем класса или объектом класса. Класс, конечно, должен быть классом исключений - потомок класса BaseException. В этом случае возбуждается исключение, тип которого определяется заданным классом, а значением соответствующий объект. Если в операторе указан класс, а не объект, то объект создается автоматически конструктором по умолчанию. Для собственных классов исключений, имеющих кортеж аргументов, при возбуждении исключения соответствующий объект следует создать.
  • Если в операторе raise помимо выражения1 указывается и from выражение 2, то это выражение также задает класс исключений или объект исключений. Такое исключение присоединяется к исключению, заданному в выражении1. Эта редко используемая форма применяется обычно в except-обработчиках, когда обработчик в ходе своей работы возбуждает собственное исключение, присоединяя к нему исключение, захваченное обработчиком.
  • Приведу пример, где обработчик исключения захватывает любое исключение. В процессе обработки выбрасывает исключение RuntimeError, присоединяя к нему захваченное исключение:

    def ff():
        try:
            x = 1
            y = [2]
            z = x + y
        except Exception as except_obj:
     raise RuntimeError("Серьезная ошибка при выполнении ff") from except_obj
    

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

    С другими примерами применения оператора raise мы сталкивались в предыдущих лекциях, в частности, при описании методов класса Rational возбуждалось исключение класса TypeError.

    Обработка исключений с подъемом по цепочке вызовов методов

    Понимание того, как обрабатывается исключительная ситуация, требует более серьезного рассмотрения. До сих пор неявно предполагалось, что исключительная ситуация возбуждается в охраняемом блоке и один из except-обработчиков этого блока выполняет обработку этой ситуации. Однако зачастую в обработке возникшей ситуации участвует ансамбль обработчиков разных охраняемых блоков. Дело в том, что в теле охраняемого блока метода f1 может содержаться вызов метода f2, в свою очередь содержащего охраняемый блок с соответствующими except-обработчиками. Транзитивно ситуация может повторяться. Предположим, что исключительная ситуация возникла в методе fk. Понятно, что возлагать всю ответственность за обработку ситуации только на except-обработчиков блока fk неразумно, - обработчики всех блоков должны принять участие в обработке, главная ответственность лежит на except-обработчиках блока f1 - метода, породившего цепочку вызовов, приведшую к появлению исключительной ситуации.

    Как строится обработка исключения в таких случаях? Первым ситуацию пытается исправить except-обработчик того блока, в котором возникла ситуация. Заканчивает он свою работу вызовом оператора raise, возбуждая активную исключительную ситуацию. В результате происходит подъем по цепочке вызовов методов и в исправление ситуации принимает очередной разработчик. На каком-то шаге ситуация может быть исправлена, исключительная ситуация не возбуждается и продолжается нормальное выполнение. Другой исход - завершение работы, когда становится понятно, что дальнейшее продолжение работы становится невозможным. Чаще всего, окончательное решение принимается при обработке ситуации в вызывающем методе f1.

    Давайте вернемся к выше приведенному примеру, где подобная ситуация имела место, хотя я не акцентировал на этом внимания. Напомню суть примера. В тесте вызывалась функция fg, в теле которой был охраняемый блок с except-обработчиками. При выполнении этой функции могли возникать исключительные ситуации двух типов. Ситуация деления на ноль возникала непосредственно в охраняемом блоке функции fg и except-обработчик этого блока исправлял ситуацию, продолжая выполнение программы. Вторая же ситуация, связанная с некорректностью типа операнда, возникала при вызове функции f. У этой функции не было охраняемого блока и соответственно except-обработчиков. Поэтому при возникновении исключительной ситуации был вызван стандартный обработчик исключения, который исправить ситуацию не в состоянии, завершить выполнение программы не вправе, он обязан передать управление вышестоящему except-обработчику, что и было сделано. Обработчик охраняемого блока функции fg смог исправить ситуацию и выполнение программы продолжилось.

    Сейчас я построю более сложный пример, проясняющий детали обработки исключения при наличии цепочки вызовов методов в теле охраняемых блоков.

    Заключительный пример

    Приведу пример, в котором:

  • Построен класс Work и связанный с ним класс ProcessError, описывающий исключительную ситуацию, которая может возникать в процессе работы методов класса Work.
  • В классе Work построены три метода: Begin, Start, Process, каждый из которых содержит охраняемый блок с соответствующим except-обработчиком.
  • Методы образуют цепочку вызовов: Begin =>l Start => Process.
  • Обработка исключительной ситуации, которая может возникать в процессе работы метода Process, выполняется ансамблем обработчиков, подъемом по цепочке вызовов: Begin <= Start <= Process.
  • В этом примере я выполню сделанное ранее обещание показать, как можно в Python моделировать схему обработки исключения с возобновлением, когда управление передается try-блоку для повторного выполнения с надеждой на нормальное продолжение работы, благодаря изменениям, сделанным ансамблем обработчиков. Идея такой схемы состоит в том, что охраняемый блок заключается в цикл. Если при работе охраняемого блока исключительная ситуация не возникает, то цикл завершает работу при однократном выполнении тела цикла. Если же исключительная ситуация возникла, то обработчики ситуации вносят изменения, при которых тело цикла на следующей итерации будет работать по-другому, например. исполнять альтернативный алгоритм, использовать новую эвристику.

    Перейду к описанию примера, моделирующего некий производственный процесс. Вот начало описания класса Work:

    import random
    from ProcessError import ProcessError
    class Work(object):
        """
        Моделирует производственный процесс
        """
        #границы показателя эффективности процесса
        
        Low = -10
        Up = 20
        res = []
        was_exception = True
    
        def __init__(self):
            pass
    

    У класса задан ряд атрибутов класса, которые будут использоваться обработчиками события, чтобы повлиять на результаты производственного процесса. Заметьте, я не стал вводить атрибуты, присущих объектам класса.

    Давайте теперь определим три взаимосвязанных метода, моделирующих некоторый производственный процесс:

    def Begin(self):
            print('Подготовка запуска процесса')
            while self.was_exception:
                self.was_exception = False
                try:
                    self.Start()
                except ProcessError as exc:
                    print('ProcessError:' +
               'Обработчик Begin пытается исправить ситуацию.' +
               'Повторно запускает процесс')
                    self.res = []
                    self.Low += 2
            print("Процесс нормальный! ")
     print(self.res)
    

    Работа начинается с вызова метода Begin, который вызывает метод Start, продолжающий работу. Заметьте, try-блок обернут в цикл while, условие которого определяется атрибутом класса - was_exception (было ли исключение). Инициализируется этот атрибут значением True, так что первый раз тело цикла будет выполняться. Но уже первый оператор тела цикла меняет это значение на False, так что если в процессе выполнения процесса исключительная ситуация не возникнет, то метод нормально завершит работу, сообщив об этом, и напечатав результат работы - список res. Если же ситуация возникнет, то метод повторно выполнит try-блок, но при этом параметры, управляющие процессом, изменятся в except-обработчиках, способствуя нормализации процесса работы. Гарантируется, что в процессе итераций нормальная работа будет достигнута.

    Вот код следующего метода Start:

    def Start(self):
            print('Запуск процесса')
            try:
                self.Process()
            except ProcessError as exc:
                print('ProcessError:' +
               'Обработчик Start пытается исправить ситуацию.' +
               'Передает управление вышестоящему обработчику')
                self.Low += 1
         raise
    

    Этот метод вызывает метод Process, непосредственно выполняющий процесс. Его event-обработчик ситуации ProcessError изменяет параметр Low,влияющий на ход процесса и в заключение повторно возбуждает исключение ProcessError, передавая управление обработчику этой ситуации в методе Begin.

    Вот код метода Process, выполняющего процесс, приводящий иногда к исключительной ситуации ProcessError, считающейся некорректной:

    def Process(self):
            s = 'Показатель эффективности процесса ниже нормы'
            try:
                count = 0
                for i in range(0, 10):
                    p = random.randint(self.Low, self.Up)
                    self.res.append(p)
                    if p < 0:
                        count += 1                    
                        if count > 2:
                            raise ProcessError(s, self.Low, self.res)
                self.was_exception = False
                return self.res
            except ProcessError as exc:
                print('ProcessError:' +
               'Обработчик Process пытается исправить ситуацию.' +
               'Передает управление вышестояшему обработчику')
                self.was_exception = True;
                self.Low += 1
                raise
    

    Производственный процесс моделируется тем, что заполняется список случайными числами. Среди 10 чисел этого списка отрицательных должно быть меньше заданной константы, иначе возникает исключительная ситуация, считается что процесс некорректен. Если исключительная ситуация возникла, то параметр was-exception принимает значение True,что приведет к повторению запуска процесса. Одновременно параметр Low увеличивается, что приводит к уменьшению вероятности появления отрицательных чисел. По завершении обработки исключения управление передается вышестоящему обработчику.

    Так устроен класс Work. Посмотрим теперь, как устроен класс ProcessError, определяющий исключительную ситуацию:

    class ProcessError(Exception):
        """
        Класс исключений для класса Work
        """
        def __init__(self, message, limit, result):
            self.message = message
            self.limit = limit
     self.result = result
    

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

    Для полноты картины приведу тест, запускающий выполнение построенной модели:

    def test3():
        from Work import Work
        work = Work()
        work.Begin()
    

    Приведу теперь результаты работы этого теста для двух сеансов работы:

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

    Страницы:

    Когда мы работаем с собственным классом и спроектированным для него классом исключений, то, понятно, исключение может появиться только в результате выполнения кода, написанного программистом. Обнаружить ситуацию, приводящую к некорректной работе класса и его объектов, создать в этом случае объект исключение, передав ему соответствующие аргументы, - все это выполняется обычными программными средствами, ведь исключение это такой же объект, как и другие объекты, с которыми работает программист. Но для того чтобы реально возникла исключительная ситуация, прерывающая выполнение программного кода, необходим в этом случае специальный оператор, который "возбуждает" исключение. Иногда говорят, что оператор "поднимает" или " выбрасывает" исключение. Таким оператором в языке Python является оператор raise. Синтаксис оператора имеет вид:

    raise [<expression1> [from <expression2>] ]
    

    Рассмотрим семантику оператора:

  • Если выражение отсутствует и задано только ключевое слово raise, то возбуждается последнее исключение, активное в области действия оператора raise. Если активного исключения нет, то возбуждается исключение класса RuntimeError.
  • Выражение1 должно быть именем класса или объектом класса. Класс, конечно, должен быть классом исключений - потомок класса BaseException. В этом случае возбуждается исключение, тип которого определяется заданным классом, а значением соответствующий объект. Если в операторе указан класс, а не объект, то объект создается автоматически конструктором по умолчанию. Для собственных классов исключений, имеющих кортеж аргументов, при возбуждении исключения соответствующий объект следует создать.
  • Если в операторе raise помимо выражения1 указывается и from выражение 2, то это выражение также задает класс исключений или объект исключений. Такое исключение присоединяется к исключению, заданному в выражении1. Эта редко используемая форма применяется обычно в except-обработчиках, когда обработчик в ходе своей работы возбуждает собственное исключение, присоединяя к нему исключение, захваченное обработчиком.
  • Приведу пример, где обработчик исключения захватывает любое исключение. В процессе обработки выбрасывает исключение RuntimeError, присоединяя к нему захваченное исключение:

    def ff():
        try:
            x = 1
            y = [2]
            z = x + y
        except Exception as except_obj:
     raise RuntimeError("Серьезная ошибка при выполнении ff") from except_obj
    

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

    С другими примерами применения оператора raise мы сталкивались в предыдущих лекциях, в частности, при описании методов класса Rational возбуждалось исключение класса TypeError.

    Обработка исключений с подъемом по цепочке вызовов методов

    Понимание того, как обрабатывается исключительная ситуация, требует более серьезного рассмотрения. До сих пор неявно предполагалось, что исключительная ситуация возбуждается в охраняемом блоке и один из except-обработчиков этого блока выполняет обработку этой ситуации. Однако зачастую в обработке возникшей ситуации участвует ансамбль обработчиков разных охраняемых блоков. Дело в том, что в теле охраняемого блока метода f1 может содержаться вызов метода f2, в свою очередь содержащего охраняемый блок с соответствующими except-обработчиками. Транзитивно ситуация может повторяться. Предположим, что исключительная ситуация возникла в методе fk. Понятно, что возлагать всю ответственность за обработку ситуации только на except-обработчиков блока fk неразумно, - обработчики всех блоков должны принять участие в обработке, главная ответственность лежит на except-обработчиках блока f1 - метода, породившего цепочку вызовов, приведшую к появлению исключительной ситуации.

    Как строится обработка исключения в таких случаях? Первым ситуацию пытается исправить except-обработчик того блока, в котором возникла ситуация. Заканчивает он свою работу вызовом оператора raise, возбуждая активную исключительную ситуацию. В результате происходит подъем по цепочке вызовов методов и в исправление ситуации принимает очередной разработчик. На каком-то шаге ситуация может быть исправлена, исключительная ситуация не возбуждается и продолжается нормальное выполнение. Другой исход - завершение работы, когда становится понятно, что дальнейшее продолжение работы становится невозможным. Чаще всего, окончательное решение принимается при обработке ситуации в вызывающем методе f1.

    Давайте вернемся к выше приведенному примеру, где подобная ситуация имела место, хотя я не акцентировал на этом внимания. Напомню суть примера. В тесте вызывалась функция fg, в теле которой был охраняемый блок с except-обработчиками. При выполнении этой функции могли возникать исключительные ситуации двух типов. Ситуация деления на ноль возникала непосредственно в охраняемом блоке функции fg и except-обработчик этого блока исправлял ситуацию, продолжая выполнение программы. Вторая же ситуация, связанная с некорректностью типа операнда, возникала при вызове функции f. У этой функции не было охраняемого блока и соответственно except-обработчиков. Поэтому при возникновении исключительной ситуации был вызван стандартный обработчик исключения, который исправить ситуацию не в состоянии, завершить выполнение программы не вправе, он обязан передать управление вышестоящему except-обработчику, что и было сделано. Обработчик охраняемого блока функции fg смог исправить ситуацию и выполнение программы продолжилось.

    Сейчас я построю более сложный пример, проясняющий детали обработки исключения при наличии цепочки вызовов методов в теле охраняемых блоков.

    Заключительный пример

    Приведу пример, в котором:

  • Построен класс Work и связанный с ним класс ProcessError, описывающий исключительную ситуацию, которая может возникать в процессе работы методов класса Work.
  • В классе Work построены три метода: Begin, Start, Process, каждый из которых содержит охраняемый блок с соответствующим except-обработчиком.
  • Методы образуют цепочку вызовов: Begin =>l Start => Process.
  • Обработка исключительной ситуации, которая может возникать в процессе работы метода Process, выполняется ансамблем обработчиков, подъемом по цепочке вызовов: Begin <= Start <= Process.
  • В этом примере я выполню сделанное ранее обещание показать, как можно в Python моделировать схему обработки исключения с возобновлением, когда управление передается try-блоку для повторного выполнения с надеждой на нормальное продолжение работы, благодаря изменениям, сделанным ансамблем обработчиков. Идея такой схемы состоит в том, что охраняемый блок заключается в цикл. Если при работе охраняемого блока исключительная ситуация не возникает, то цикл завершает работу при однократном выполнении тела цикла. Если же исключительная ситуация возникла, то обработчики ситуации вносят изменения, при которых тело цикла на следующей итерации будет работать по-другому, например. исполнять альтернативный алгоритм, использовать новую эвристику.

    Перейду к описанию примера, моделирующего некий производственный процесс. Вот начало описания класса Work:

    import random
    from ProcessError import ProcessError
    class Work(object):
        """
        Моделирует производственный процесс
        """
        #границы показателя эффективности процесса
        
        Low = -10
        Up = 20
        res = []
        was_exception = True
    
        def __init__(self):
            pass
    

    У класса задан ряд атрибутов класса, которые будут использоваться обработчиками события, чтобы повлиять на результаты производственного процесса. Заметьте, я не стал вводить атрибуты, присущих объектам класса.

    Давайте теперь определим три взаимосвязанных метода, моделирующих некоторый производственный процесс:

    def Begin(self):
            print('Подготовка запуска процесса')
            while self.was_exception:
                self.was_exception = False
                try:
                    self.Start()
                except ProcessError as exc:
                    print('ProcessError:' +
               'Обработчик Begin пытается исправить ситуацию.' +
               'Повторно запускает процесс')
                    self.res = []
                    self.Low += 2
            print("Процесс нормальный! ")
     print(self.res)
    

    Работа начинается с вызова метода Begin, который вызывает метод Start, продолжающий работу. Заметьте, try-блок обернут в цикл while, условие которого определяется атрибутом класса - was_exception (было ли исключение). Инициализируется этот атрибут значением True, так что первый раз тело цикла будет выполняться. Но уже первый оператор тела цикла меняет это значение на False, так что если в процессе выполнения процесса исключительная ситуация не возникнет, то метод нормально завершит работу, сообщив об этом, и напечатав результат работы - список res. Если же ситуация возникнет, то метод повторно выполнит try-блок, но при этом параметры, управляющие процессом, изменятся в except-обработчиках, способствуя нормализации процесса работы. Гарантируется, что в процессе итераций нормальная работа будет достигнута.

    Вот код следующего метода Start:

    def Start(self):
            print('Запуск процесса')
            try:
                self.Process()
            except ProcessError as exc:
                print('ProcessError:' +
               'Обработчик Start пытается исправить ситуацию.' +
               'Передает управление вышестоящему обработчику')
                self.Low += 1
         raise
    

    Этот метод вызывает метод Process, непосредственно выполняющий процесс. Его event-обработчик ситуации ProcessError изменяет параметр Low,влияющий на ход процесса и в заключение повторно возбуждает исключение ProcessError, передавая управление обработчику этой ситуации в методе Begin.

    Вот код метода Process, выполняющего процесс, приводящий иногда к исключительной ситуации ProcessError, считающейся некорректной:

    def Process(self):
            s = 'Показатель эффективности процесса ниже нормы'
            try:
                count = 0
                for i in range(0, 10):
                    p = random.randint(self.Low, self.Up)
                    self.res.append(p)
                    if p < 0:
                        count += 1                    
                        if count > 2:
                            raise ProcessError(s, self.Low, self.res)
                self.was_exception = False
                return self.res
            except ProcessError as exc:
                print('ProcessError:' +
               'Обработчик Process пытается исправить ситуацию.' +
               'Передает управление вышестояшему обработчику')
                self.was_exception = True;
                self.Low += 1
                raise
    

    Производственный процесс моделируется тем, что заполняется список случайными числами. Среди 10 чисел этого списка отрицательных должно быть меньше заданной константы, иначе возникает исключительная ситуация, считается что процесс некорректен. Если исключительная ситуация возникла, то параметр was-exception принимает значение True,что приведет к повторению запуска процесса. Одновременно параметр Low увеличивается, что приводит к уменьшению вероятности появления отрицательных чисел. По завершении обработки исключения управление передается вышестоящему обработчику.

    Так устроен класс Work. Посмотрим теперь, как устроен класс ProcessError, определяющий исключительную ситуацию:

    class ProcessError(Exception):
        """
        Класс исключений для класса Work
        """
        def __init__(self, message, limit, result):
            self.message = message
            self.limit = limit
     self.result = result
    

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

    Для полноты картины приведу тест, запускающий выполнение построенной модели:

    def test3():
        from Work import Work
        work = Work()
        work.Begin()
    

    Приведу теперь результаты работы этого теста для двух сеансов работы:

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

    Вернуться к учебному плану