В этом руководстве вы узнаете, как обрабатывать ошибки в Python с точки зрения всей системы. Обработка ошибок является критическим аспектом проектирования, и она переходит от самых низких уровней (иногда аппаратных) до конечных пользователей. Если у вас нет согласованной стратегии, ваша система будет ненадежной, пользовательский интерфейс будет плохим, и у вас будет много проблем отладки и устранения неполадок.
Ключом к успеху является знание всех этих взаимосвязанных аспектов, их четкое рассмотрение и формирование решения для каждой точки.
Коды состояния и исключения
Существует две основные модели обработки ошибок: коды состояния и исключения. Коды состояния могут использоваться любым языком программирования. Исключения требуют поддержки языка / времени выполнения.
Python поддерживает исключения. Python и его стандартная библиотека свободно используют исключения, чтобы сообщать о многих исключительных ситуациях, таких как ошибки ввода-вывода, деление на ноль, индексация за пределами границ, а также о некоторых не столь исключительных ситуациях, как конец итерации (хотя это скрыто). Большинство библиотек следуют их примеру и поднимают исключения.
Это означает, что ваш код должен обрабатывать все исключения, возникающие в Python и библиотеках, так что вы также можете вызывать исключения из своего кода, когда это необходимо, и не полагаться на коды состояния.
Быстрый пример
Прежде чем погрузиться во внутреннее святилище исключений Python и рекомендации по обработке ошибок, давайте посмотрим на обработку некоторых исключений в действии:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
def f():
return 4 / 0
def g():
raise Exception(«Don’t call us. We’ll call you»)
def h():
try:
f()
except Exception as e:
print(e)
try:
g()
except Exception as e:
print(e)
|
Вот вывод при вызове h()
:
1
2
3
4
5
|
h()
division by zero
Don’t call us.
|
Python Исключения
Исключения Python — это объекты, организованные в иерархии классов.
Вот вся иерархия:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
BaseException
+— SystemExit
+— KeyboardInterrupt
+— GeneratorExit
+— Exception
+— StopIteration
+— StandardError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+— Warning
+— DeprecationWarning
+— PendingDeprecationWarning
+— RuntimeWarning
+— SyntaxWarning
+— UserWarning
+— FutureWarning
+— ImportWarning
+— UnicodeWarning
+— BytesWarning
|
Есть несколько особых исключений, которые являются производными непосредственно от BaseException
, такие как SystemExit
, KeyboardInterrupt
и GeneratorExit
. Затем существует класс Exception
, который является базовым классом для StopIteration
, StandardError
и Warning
. Все стандартные ошибки получены из StandardError
.
Когда вы вызываете исключение или вызываемую вами функцию, возникает исключение, этот нормальный поток кода завершается, и исключение начинает распространяться вверх по стеку вызовов, пока не встретит надлежащий обработчик исключения. Если обработчик исключений не доступен для обработки, процесс (или, точнее, текущий поток) будет завершен с необработанным сообщением об исключении.
Возбуждение исключений
Вызывать исключения очень легко. Вы просто используете ключевое слово raise
чтобы поднять объект, который является подклассом класса Exception
. Это может быть экземпляр самого Exception
, одно из стандартных исключений (например, RuntimeError
) или подкласс Exception
вы получили сами. Вот небольшой фрагмент, который демонстрирует все случаи:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
# Raise an instance of the Exception class itself
raise Exception(‘Ummm… something is wrong’)
# Raise an instance of the RuntimeError class
raise RuntimeError(‘Ummm… something is wrong’)
# Raise a custom subclass of Exception that keeps the timestamp the exception was created
from datetime import datetime
class SuperError(Exception):
def __init__(self, message):
Exception.__init__(message)
self.when = datetime.now()
raise SuperError(‘Ummm… something is wrong’)
|
Ловить исключения
Вы перехватываете исключения с предложением » except
, как вы видели в примере. Когда вы ловите исключение, у вас есть три варианта:
- Глотайте его тихо (держите его и продолжайте бежать).
- Сделайте что-то вроде регистрации, но повторно вызовите то же исключение, чтобы позволить более высоким уровням обрабатывать.
- Поднимите другое исключение вместо оригинала.
Проглотить исключение
Вам следует проглотить исключение, если вы знаете, как с ним справиться и можете полностью восстановиться.
Например, если вы получили входной файл, который может быть в разных форматах (JSON, YAML), вы можете попробовать проанализировать его, используя разные парсеры. Если анализатор JSON вызвал исключение, что этот файл не является допустимым файлом JSON, вы проглотите его и попробуйте использовать синтаксический анализатор YAML. Если синтаксический анализатор YAML тоже не удался, вы разрешаете распространению исключения.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
import json
import yaml
def parse_file(filename):
try:
return json.load(open(filename))
except json.JSONDecodeError
return yaml.load(open(filename))
|
Обратите внимание, что другие исключения (например, файл не найден или нет разрешений на чтение) будут распространяться и не будут учитываться специальным исключением, кроме пункта. Это хорошая политика в этом случае, когда вы хотите попробовать разбор YAML, только если разбор JSON не удался из-за проблемы кодирования JSON.
Если вы хотите обработать все исключения, используйте except Exception
. Например:
1
2
3
4
5
6
7
8
9
|
def print_exception_type(func, *args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print type(e)
|
Обратите внимание, что добавляя as e
, вы связываете объект исключения с именем e
доступным в вашем предложении, кроме.
Возвратите то же исключение
Для повторного рейза просто добавьте raise
без аргументов внутри вашего обработчика. Это позволяет вам выполнять некоторую локальную обработку, но все же позволяет верхним уровням обрабатывать это тоже. Здесь invoke_function()
выводит тип исключения на консоль, а затем повторно вызывает исключение.
01
02
03
04
05
06
07
08
09
10
11
|
def invoke_function(func, *args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
print type(e)
raise
|
Поднимите другое исключение
Есть несколько случаев, когда вы хотели бы поднять другое исключение. Иногда вы хотите сгруппировать несколько различных низкоуровневых исключений в одну категорию, которая обрабатывается кодом более высокого уровня. В случаях необходимости вам нужно преобразовать исключение в пользовательский уровень и предоставить некоторый контекст для конкретного приложения.
Наконец, пункт
Иногда вы хотите убедиться, что какой-то код очистки выполняется, даже если где-то возникло исключение. Например, у вас может быть соединение с базой данных, которое вы хотите закрыть, как только закончите. Вот неправильный способ сделать это:
1
2
3
4
5
6
7
|
def fetch_some_data():
db = open_db_connection()
query(db)
close_db_Connection(db)
|
Если функция query()
вызывает исключение, то вызов close_db_connection()
никогда не будет выполнен, а соединение с БД останется открытым. Предложение finally
всегда выполняется после выполнения обработчика исключений try all. Вот как это сделать правильно:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
def fetch_some_data():
db = None
try:
db = open_db_connection()
query(db)
finally:
if db is not None:
close_db_connection(db)
|
Вызов open_db_connection()
не может вернуть соединение или вызвать само исключение. В этом случае нет необходимости закрывать соединение с БД.
При использовании finally
, вы должны быть осторожны, чтобы не вызывать никаких исключений, потому что они будут маскировать исходное исключение.
Контекстные менеджеры
Диспетчеры контекста предоставляют еще один механизм для оборачивания ресурсов, таких как файлы или соединения с БД, в код очистки, который выполняется автоматически даже при возникновении исключений. Вместо блоков try-finally вы используете оператор with
. Вот пример с файлом:
1
2
3
4
5
|
def process_file(filename):
with open(filename) as f:
process(f.read())
|
Теперь, даже если process()
вызвал исключение, файл будет правильно закрыт сразу после выхода из области действия блока, независимо от того, было ли обработано исключение или нет.
логирование
Ведение журнала является в значительной степени требованием в нетривиальных, длительно работающих системах. Это особенно полезно в веб-приложениях, где вы можете обрабатывать все исключения общим образом: просто зарегистрируйте исключение и верните сообщение об ошибке вызывающей стороне.
При ведении журнала полезно регистрировать тип исключения, сообщение об ошибке и трассировку стека. Вся эта информация доступна через объект sys.exc_info
, но если вы используете метод logger.exception()
в вашем обработчике исключений, система регистрации Python извлечет всю необходимую для вас информацию.
Это лучшая практика, которую я рекомендую:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
import logging
logger = logging.getLogger()
def f():
try:
flaky_func()
except Exception:
logger.exception()
raise
|
Если вы будете следовать этой схеме, то (при условии, что вы правильно настроили ведение журнала), что бы ни случилось, у вас в журналах будет довольно хорошая запись о том, что пошло не так, и вы сможете решить эту проблему.
Если вы сделаете ререйз, убедитесь, что вы не регистрируете одно и то же исключение снова и снова на разных уровнях. Это пустая трата времени, и это может сбить вас с толку и заставить вас думать, что произошло несколько экземпляров одной и той же проблемы, когда на практике один экземпляр регистрировался несколько раз.
Самый простой способ сделать это — позволить всем исключениям распространяться (если они не могут быть обработаны уверенно и проглочены ранее), а затем вести запись близко к верхнему уровню вашего приложения / системы.
караул
Регистрация это возможность. Наиболее распространенной реализацией является использование файлов журнала. Но для крупномасштабных распределенных систем с сотнями, тысячами и более серверами это не всегда лучшее решение.
Чтобы отслеживать исключения во всей вашей инфраструктуре, такая служба, как часовая , очень полезна. Он централизует все отчеты об исключениях и в дополнение к трассировке стека добавляет состояние каждого кадра стека (значение переменных на момент возникновения исключения). Он также предоставляет действительно хороший интерфейс с панелями мониторинга, отчетами и способами разбивки сообщений по нескольким проектам. Это открытый исходный код, поэтому вы можете запустить свой собственный сервер или подписаться на размещенную версию.
Работа с переходной ошибкой
Некоторые сбои носят временный характер, особенно при работе с распределенными системами. Система, которая волнуется при первых признаках проблемы, не очень полезна.
Если ваш код обращается к какой-то удаленной системе, которая не отвечает, традиционным решением являются тайм-ауты, но иногда не каждая система разработана с тайм-аутами. Таймауты не всегда легко откалибровать при изменении условий.
Другой подход состоит в том, чтобы быстро потерпеть неудачу и затем повторить попытку Преимущество состоит в том, что если цель реагирует быстро, вам не нужно проводить много времени в состоянии сна и вы можете реагировать немедленно. Но если это не удалось, вы можете повторить попытку несколько раз, пока не решите, что это действительно недоступно, и вызовете исключение. В следующем разделе я представлю декоратор, который может сделать это для вас.
Полезные декораторы
Двумя декораторами, которые могут помочь с обработкой ошибок, являются @log_error
, который регистрирует исключение и затем повторно вызывает его, и @retry
декоратор, который будет повторять вызов функции несколько раз.
Регистратор ошибок
Вот простая реализация. Декоратор исключает объект логгера. Когда он декорирует функцию и вызывается, она обернет вызов в предложение try-exclude, и если возникло исключение, оно зарегистрирует его и, наконец, повторно вызовет исключение.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
def log_error(logger)
def decorated(f):
@functools.wraps(f)
def wrapped(*args, **kwargs):
try:
return f(*args, **kwargs)
except Exception as e:
if logger:
logger.exception(e)
raise
return wrapped
return decorated
|
Вот как это использовать:
01
02
03
04
05
06
07
08
09
10
11
|
import logging
logger = logging.getLogger()
@log_error(logger)
def f():
raise Exception(‘I am exceptional’)
|
Retrier
Вот очень хорошая реализация декоратора @retry .
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
import time
import math
# Retry decorator with exponential backoff
def retry(tries, delay=3, backoff=2):
»’Retries a function or method until it returns True.
delay sets the initial delay in seconds, and backoff sets the factor by which
the delay should lengthen after each failure.
or else it isn’t really a backoff.
greater than 0.»’
if backoff <= 1:
raise ValueError(«backoff must be greater than 1»)
tries = math.floor(tries)
if tries < 0:
raise ValueError(«tries must be 0 or greater»)
if delay <= 0:
raise ValueError(«delay must be greater than 0»)
def deco_retry(f):
def f_retry(*args, **kwargs):
mtries, mdelay = tries, delay # make mutable
rv = f(*args, **kwargs) # first attempt
while mtries > 0:
if rv is True: # Done on success
return True
mtries -= 1 # consume an attempt
time.sleep(mdelay) # wait…
mdelay *= backoff # make future wait longer
rv = f(*args, **kwargs) # Try again
return False # Ran out of tries 🙁
return f_retry # true decorator -> decorated function
return deco_retry # @retry(arg[, …]) -> true decorator
|
Вывод
Обработка ошибок имеет решающее значение как для пользователей, так и для разработчиков. Python обеспечивает отличную поддержку языка и стандартной библиотеки для обработки ошибок на основе исключений. Прилежно следуя лучшим практикам, вы можете преодолеть этот часто пренебрегаемый аспект.
Выучить питон
Изучите Python с нашим полным руководством по питону, независимо от того, начинаете ли вы или начинающий программист, ищущий новые навыки.