Комментарии 5
Неплохая статья, только непонятно, почему в тегах на первом месте Питон. Имхо, эти же паттерны вполне полезны и программистам на многих других языках. У меня, например, фортран, но практически все рекомендации вполне адекватны (с точностью до механизма реализации). Больше того, я начал ими пользоваться еще за несколько лет до появления Питона как языка ;-)
Единственное дополнение к статье - про взаимодействие с юзером. Иногда прога должна сообщить, что что-то пошло не так, но после этого можно продолжить работу (восстанавливаемая ошибка). Однако, если такие сообщения повторяются слишком часто, то это быстро начнет раздражать. В такой ситуации есть смысл ограничивать количество повторных сообщений.
Например, у меня прога рисует графики временных рядов, а по нажатию кнопочки накладывает на эти графики стрелочки землетрясений. Или не накладывает, если в окне нет подходящих событий для отображения. Во втором случае надо показать MessageBox, чтобы его действие не проваливалось в пустоту. Но обычно при просмотре графиков юзер все время смещает окно вправо-влево, разворачивает и сворачивает, и т.д. Землетрясения, естественно, перерисовываются в каждом новом окне заново. Постоянное появление подобного MessageBox начинает раздражать уже со второго-третьего раза. В такой ситуации я на второй-третий раз добавляю к основному тексту "Больше не буду об этом предупреждать" (отдельной строкой, чтобы изменение сообщения сразу бросалось в глаза), и затем выключаю его вплоть до переоткрытия экрана графиков.
Для примера, под спойлером функция на фортране, которая это делает. Если Вы не знакомы с современным фортраном, и любите острые ощущения, то очень советую заглянуть ;-) Вас ждет незабываемый когнитивный диссонанс между ожиданиями от кода на фортране (который по определению должен быть нечитаемым для непосвященных), и реальностью ;-)
Код
CCCCCC SUBR DISPLAY_EVENTS(catalog_name,max_msg_repeater) CCCCCCCCC
C с*(*) i*4 С
C С
C Процедура читает события из файла catalog_name = каталога землетрясений C
C и рисует стрелочки на экране с графиками временных рядов C
C Входные параметры: C
C catalog_name - имя файла с каталогом зтр C
C max_msg_repeater - максимальное кол-во повторных сообщений C
C Используются глобальные настройки формата файла-каталога и критериев выборки зтр
C Побочные эффекты: в случае отсутствия зтр пишет в лог критерии выборки C
C...............................................................................C
SUBROUTINE DISPLAY_EVENTS(catalog_name,max_msg_repeater)
USE CATALOG_INC ! Модуль с описанием объекта EVENT и функций для работы с ней (фактически = класс)
character (len=*), intent(in) :: catalog_name ! Неизменяемый входной параметр
integer*4, intent(in) :: max_msg_repeater ! Неизменяемый входной параметр
character (len=1024) :: msg_text ! Каталогов с длиной имени больше 512 символов не бывает ;-)
integer*4, save :: no_events_counter=0 ! Счетчик- локальный, но сохраняет значение между вызовами
! При первом вызове функции он будет устанолвлен в 0
integer*4 :: displayed_events ! Счетчик нарисованных землетрясений
c
c
c.....1. Цикл читает файл каталога и рисует стрелочку события на графике, ЕСЛИ
c землетрясение попало в текущее временнОе окно И удовлетворяет
c текущим ограничениям на параметры отображаемых событий (магнитуда и др.):
if (.not.open_catalog(catalog_name)) return ! Обработка ошибок (= сообщение юзеру) внутри
displayed_events=0
do while (get_event(event)) ! Побочный эффект get_event: close(catalog) по концу файла
if (display_event(event)) displayed_events=displayed_events+1 ! функция вернет Ok (=True), если она что-то нарисовала
end do
if (displayed_events > 0) return
! Каталог открывается явным вызовом, но закрывать его яно не нужно: это сделала get_event
! Такой стиль не приводит к ошибкам, т.к. параметры потока хрянятся в соотв.классе (он же
! делает буферизацию и др.), и многократное закрытие одного и того же потока не грозит ошибками
! Также он автоматически закроется при новом вызове open либо при выходе из экрана графиков
c
c
c.....999. Блок обработки ошибок.
c В более сложных функциях этот блок у меня обычно начинается с метки 999,
c а в теле функции можно местами встретить разные проверки с последующим GOTO 999
c (перед каждым goto устанавливается номер ошибки и т.д.)
c Да-да, я в курсе, что "goto" - это плохо ;-)
c Но вот именно в таком контексте я считаю его использование оправданным
c и полезным, т.к. читаемость кода в результате лишь улучшается
c
c Обработка ошибок: печатаем сообщение не более Max_msg_repeater раз:
no_events_counter=no_events_counter+1
if (no_events_counter > Max_msg_repeater) return ! Уходим, не прощаясь
c
c Собственно печать. Символ "|" у меня означает переход на новую строку:
msg_text='В каталоге '//trim(catalog_name)//'|нет ни одного события, удовлетворяющего условиям выборки'
if (no_events_counter == Max_msg_repeater) msg_text=trim(msg_text)//'||Больше не буду об этом предупреждать.'
call display_msg(msg_text)
end
На самом деле реальная функция чуть сложнее (код здесь): она сообщает не только про отсутствие событий, но и поясняет, какой из критериев выборки сыграл фатальную роль ;-) Здесь я немного ее упростил для лучшего понимания главной идеи.
Хорошая статья, спасибо за перевод. Мне не очень понятно отличие завершения приложения на проде от "падения" со стектрейсом... В любом случае приложение завершается. Разве что, в случае контролируемого завершения приложение может как-то уведомить разработчиков, но это уже ответственность не приложения, а "оркестратора".
Хотелось бы ещё разобраться что такое raise from e
и raise from None
, где он полезен, где необходим, где вреден. Никак не получается это уложить в голове
Находясь в обработчике исключений у вас есть 5 вариантов, что вы можете сделать:
Обработать исключение и подавить его
Обработать исключение и пробросить дальше - просто вызвать raise
Обработать исключение и выбросить новое исключение - вызвать raise Exception()
Обработать исключение и выбросить новое исключение и сохранить информацию об оригинальном - вызвать raise Exception() from e
Обработать исключение и выбросить новое исключение, но скрыть информацию об оригинальном исключении - вызвать raise Exception() from None
def func():
raise ConnectionError
# 1 вариант - обрабатывает и подавляет исключение
try:
func()
except ConnectionError as exc:
print('exception: ', type(exc), exc)
# получите сообщение:
# exception: <class 'ConnectionError'>
# 2 вариант - обрабатывает и пробрасывает оригинальное исключение дальше
try:
func()
except ConnectionError as exc:
print(exc)
raise
# получите следующее сообщение:
# exception: <class 'ConnectionError'>
# Traceback (most recent call last):
# File ".\z.py", line 6, in <module>
# func()
# File ".\z.py", line 2, in func
# raise ConnectionError
# ConnectionError
# 3 вариант - обработать и выбросить новое исключение из обработчика
# это будет воспринято, как произошло новое исключение, во время обработки оригинального
try:
func()
except ConnectionError as exc:
raise RuntimeError('Failed to open database')
# получите сообщение:
# Traceback (most recent call last):
# File ".\z.py", line 6, in <module>
# func()
# File ".\z.py", line 2, in func
# raise ConnectionError
# ConnectionError
# During handling of the above exception, another exception occurred:
# Traceback (most recent call last):
# File ".\z.py", line 8, in <module>
# raise RuntimeError('Failed to open database')# from None
# RuntimeError: Failed to open database
# 4 вариант - обработать и выбросить исключение (например, кастомное исключение вашей библиотеки)
# и сохранить информацию об оригинальном исключении
try:
func()
except ConnectionError as exc:
raise RuntimeError('Failed to open database') from exc
# получите следующее сообщение:
# Traceback (most recent call last):
# File ".\z.py", line 6, in <module>
# func()
# File ".\z.py", line 2, in func
# raise ConnectionError
# ConnectionError
# The above exception was the direct cause of the following exception:
# Traceback (most recent call last):
# File ".\z.py", line 8, in <module>
# raise RuntimeError('Failed to open database') from exc
# RuntimeError: Failed to open database
# Заметьте разницу в сообщении об оригинальном исключении:
# было During handling of the above exception, another exception occurred
# стало The above exception was the direct cause of the following exception
# разница я думаю понятна
# 5 вариант - обработать и выбросить исключение (например, кастомное исключение вашей библиотеки)
# и скрыть информацию об оригинальном сообщении:
try:
func()
except ConnectionError as exc:
raise RuntimeError('Failed to open database') from None
# получите следующее сообщение:
# Traceback (most recent call last):
# File ".\z.py", line 8, in <module>
# raise RuntimeError('Failed to open database') from None
# RuntimeError: Failed to open database
# как можете видеть - оригинальное исключение скрыто
PS. Простите за спам в почте - я победил редактор
Наверное, надо еще написать, где это полезно (на мой взгляд):
Первый вариант (ИМХО) допустим на вершине стека, когда дальше передавать исключение некуда.
Второй вариант - когда в данном месте вам нужна дополнительная обработка исключения (могу только придумать, насытить исключение дополнительным контекстом)
Третий вариант - представьте что у вас в 1М строк кода и в логах вы начинаете видеть During handling of the above exception, another exception occurred - для вас как для инженера это означает одно - произошла еще одна ошибка при обработке существующей и не известно, все ли пошло по плану - соответственно надо разбираться, что за ошибка.
Четвертый вариант - вы оборачиваете исключения либо в свои библиотечные, либо просто сокращаете количество исключений, которые могут от вас вылететь (снижая нагрузку на вызывающий код), но при этом сохраняете оригинальное исключение (например, чтобы оно попало в лог и потом кто-то его расследовал)
Пятый вариант как четвертый, но оригинальное сообщение вам не нужно, потому что в новом достаточно информации.
Полное руководство по обработке ошибок в Python