Обработка исключений на нескольких уровнях вызова

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

Я в основном из Java. Там я бы просто объявлял любые методы как throws Exception, перебрасывал и ловил где-то на верхнем уровне.

Однако Python отличается. Мой код Python в основном выглядит так, как показано ниже.

EDIT: добавлен гораздо более простой код...

Основная функция входа (plugin.py):

def main(catalog):

    print "Executing main(catalog)... "
    # instantiate generator
    gen = JpaAnnotatedClassGenerator(options)

    # run generator
    try:
        gen.generate_bar()  # doesn't bubble up
    except ValueError as error:
        Utilities.show_error("Error", error.message, "OK", "", "")
        return

    ... usually do the real work here if no error

JpaAnnotatedClassGenerator класс (engine.py):

class JpaAnnotatedClassGenerator:

    def generate_bar(self):
        self.generate_value_error()

    def generate_value_error(self):
        raise ValueError("generate_value_error() raised an error!")

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

ВОПРОС: как это лучше всего сделать в Python? Мне действительно нужно повторять try-except для каждого вызываемого метода?

Кстати: я использую Python 2.6.x и не могу обновиться из-за привязки к MySQL Workbench, который предоставляет интерпретатор (Python 3 находится в их списке обновлений).


person Kawu    schedule 26.02.2019    source источник
comment
Не нужно писать Python: в заголовке. Достаточно пометить его с помощью Python. Не знаю, почему никто не сказал вам этого раньше. Я вижу, вы сделали это, по крайней мере, в каждом другом заданном вами вопросе. Вот почему я упоминаю об этом.   -  person Torxed    schedule 27.02.2019
comment
Может быть. Я надеялся, что поисковые системы смогут лучше различать вопросы, относящиеся к языку, если он не будет скрыт в каком-то теге. В конце концов, упоминается и Java. Хотя я не специалист по SEO   -  person Kawu    schedule 27.02.2019
comment
Не уверен, что это поможет в вашем случае использования, но я думаю, что эквивалентом параметра функции, как в java, было бы определение декоратора функции, который вызывает исключение и применяет его к вашим функциям. stackoverflow.com/questions/40597683/   -  person Simon Hibbs    schedule 27.02.2019
comment
Обратите внимание, что Generator — это встроенный тип (функция) в Python. Повторное использование имени может запутать сопровождающих.   -  person MisterMiyagi    schedule 27.02.2019
comment
Я переименовал класс при публикации этого вопроса. Фактическое имя класса JpaAnnotatedClassGenerator. См. редактирование.   -  person Kawu    schedule 27.02.2019


Ответы (2)


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

Из этого POV ваш код кажется наиболее правильным (предупреждение: я не удосужился прочитать все это, просто быстро просмотрел), за исключением (без каламбура) для пары моментов:

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

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

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

elif AnnotationUtil.is_embeddable_table(table) and AnnotationUtil.is_secondary_table(table):
    # ...
elif AnnotationUtil.is_embeddable_table(table):
    # ...
elif AnnotationUtil.is_secondary_table(table):
    # ...

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

РЕДАКТИРОВАТЬ:

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

Вы можете легко проверить правильность его работы с помощью простого MCVE:

def deeply_nested():
    raise ValueError("foo")

def nested():
    return deeply_nested()

def firstline():
    return nested()

def main():
    try:
        firstline()
    except ValueError as e:
        print("got {}".format(e))
    else:
        print("you will not see me")

if __name__ == "__main__":
    main()

Похоже, что программное обеспечение, поставляющее Python env, каким-то образом неправильно обрабатывает основной файл плагина. Похоже, мне придется проверить ребят из MySQL Workbench.

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

person bruno desthuilliers    schedule 27.02.2019
comment
Ты прав. Я использую ValueError только потому, что в то время у меня были другие проблемы, помимо определения пользовательского PluginError. Я отложил это до тех пор, пока проблема, описанная в моем вопросе, не станет приоритетной. ;-) И да, код, который вы выложили, я оптимизирую обращения к AnnotationUtil.is_*(). юк - person Kawu; 27.02.2019
comment
Я наткнулся на трудно найти ошибку в моем коде. Спасибо за помощь в объяснении того, как обрабатываются исключения в Python. - person Kawu; 27.02.2019

Это нормально, когда ошибки всплывают

Исключения Python не проверяются, что означает, что вы не обязаны объявлять или обрабатывать их. Даже если вы знаете, что что-то может подняться, ловите ошибку только в том случае, если вы собираетесь что-то с ней делать. Хорошо иметь прозрачные для исключений слои, которые изящно прерываются, когда через них всплывает исключение:

def logged_get(map: dict, key: str):
    result = map[key]  # this may raise, but there is no state to corrupt
    # the following is not meaningful if an exception occurred
    # it is fine for it to be skipped by the exception bubbling up
    print(map, '[%s]' % key, '=>', result)
    return result

В этом случае logged_get будет просто пересылать любые KeyError (и другие), которые возникают при поиске. Если внешний вызывающий объект знает, как обработать ошибку, он может это сделать.

Итак, просто позвоните self.create_collection_embeddable_class_stub так, как вы это делаете.

Это нормально, если ошибки убивают приложение

Даже если ничто не обрабатывает ошибку, это делает интерпретатор. Вы получаете трассировку стека, показывающую, что и где пошло не так. Фатальные ошибки типа «происходит только при наличии ошибки» могут «безопасно» всплывать, чтобы показать, что пошло не так.

На самом деле выход из интерпретатора и утверждения также используют этот механизм.

>>> assert 2 < 1, "This should never happen"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
AssertionError: This should never happen

Для многих служб вы можете использовать это даже при развертывании — например, systemd запишет это для системной службы Linux. Старайтесь подавлять внешние ошибки только в том случае, если безопасность является проблемой или если пользователи не могут обработать ошибку.

Можно использовать точные ошибки

Поскольку исключения не проверяются, вы можете использовать произвольное количество без перегрузки API. Это позволяет использовать пользовательские ошибки, сигнализирующие о разных уровнях проблем:

class DBProblem(Exception):
    """Something is wrong about our DB..."""

class DBEntryInconsistent(DBProblem):
    """A single entry is broken"""

class DBInconsistent(DBProblem):
    """The entire DB is foobar!"""

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

try:
    gen.generate_classes(catalog)
except DBEntryInconsistent:
    logger.error("aborting due to corrupted entry")
    sys.exit(1)
except DBInconsistent as err:
    logger.error("aborting due to corrupted DB")
    Utility.inform_db_support(err)
    sys.exit(1)
# do not handle ValueError, KeyError, MemoryError, ...
# they will show up as a stack trace
person MisterMiyagi    schedule 27.02.2019
comment
Не уверен, правильно ли я понял вас и Бруно, но проблема в том, что вызов вложенной функции create_collection_embeddable_class_stub(...) НЕ поднимается до try-except функции main(). Я только что попробовал. См. строку 19 2-го поля кода, в части elif AnnotationUtil.is_embeddable_table(table): ... embeddable_class = self.create_collection_embeddable_class_stub(table, self.common_table_prefix) - person Kawu; 27.02.2019
comment
@Kawu Исключения из вложенных функций do всплывают. Так работает Python. Боюсь, ваш код слишком обширен, чтобы подробно отслеживать, что происходит. Вы уверены, что исключение действительно выброшено, т.е. достигнут путь кода? - person MisterMiyagi; 27.02.2019
comment
Это странно. Я только что попробовал кое-что. При попытке поймать ошибку в функции main() исключения НЕ всплывают, но когда я использую этот шаблон на один уровень глубже, всплывающее окно, кажется, работает. Похоже, что программное обеспечение, поставляющее Python env, каким-то образом неправильно обрабатывает основной файл плагина. Похоже, мне придется проверить ребят из MySQL Workbench... - person Kawu; 27.02.2019
comment
Ах. Наконец нашел проблему. Проблемы с логикой кода. Иногда неспособность использовать отладчик является сложной задачей. Спасибо за помощь. - person Kawu; 27.02.2019