Стратегии контроля ошибок
Contents
Стратегии контроля ошибок#
Как уже упоминалось, возникновение исключений в python
не всегда является следствием логических ошибок программы. Нередко механизм исключений и их обработки используется для упрощения программы и повышения её надежности.
LBYL vs EAFP#
В этой связи часто рассматривают два кардинально разных подхода к контролю ошибок.
Первый из них — LBYL, который расшифровывается “Look Before You Leap” (осмотрись, прежде чем прыгать), который предписывает перед попыткой выполнить какую-то операцию убедиться, что её выполнению ничто не помешает. При таком подходе в коде обычно встречается большое количество проверок
if
.Второй из них — EAFP, который расшифровывается “It’s Easier to Ask Forgivness than Permission” (проще просить прощения, чем получить разрешение), который предписывает попытаться выполнить операцию в блоке
try
и обработать возможные исключения в блокахexcept
.
В python
в большинстве ситуаций второй подход является предпочтительным. Разберем основные недостатки первого подхода.
Во-первых, проверочный код может ухудшить читабельность программы и затруднить понимании основной её части, выполняющейся в отсутствии ошибок.
В качестве примера, предположим, что мы хотим написать функцию безопасного вычисления функции \(f(x) = \sqrt{(x-1)(x-2)(x-3)}\), которая определена на множестве \([1, 2]\cup[3,\infty]\).
from math import sqrt
def f(x):
return sqrt((x-1)*(x-2)*(x-3))
Вызов этой функции с аргументом вне области определения приведет к возникновению исключения ValueError
.
f(2.5)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_5124/3994200981.py in <module>
----> 1 f(2.5)
~\AppData\Local\Temp/ipykernel_5124/2484835386.py in f(x)
2
3 def f(x):
----> 4 return sqrt((x-1)*(x-2)*(x-3))
ValueError: math domain error
Реализуем функцию, которая будет возвращать \(f(x)\), если \(x\) из области определения и None
иначе.
LBYL
def safe_f(x):
if x < 0:
return None
if 2 < x < 3:
return None
return sqrt((x-1)*(x-2)*(x-3))
При чтении такой реализации в стиле LBYL первое, что бросается в глаза, — проверки на значение аргумента \(x\), в то время как основной код оказывается скрытым где-то в конце функции.
EAFP
def safe_f(x):
try:
return sqrt((x-1)*(x-2)*(x-3))
except ValueError:
return None
При подходе EAFP основной код располагается наверху функции, а исключительные ситуации располагаются ниже.
Во-вторых, проверочный код может дублировать действия, которые выполняются при совершении самой операции.
В качестве примера рассмотрим метод get словарей и его возможный реализации. Напомним, что get
возвращает значение по ключу, если такой ключ находится, и возвращает None
иначе.
LBYL
def get(d, k):
if k in d:
return d[k]
else:
return None
Заметим, что выполнение обеих инструкций k in d
и d[k]
приводит к проверке наличия ключа в словаре.
EAFP
def get(d, k):
try:
return d[k]
except KeyError:
return None
Проверка на наличие ключа происходит только в операции d[k]
.
Реализация в рамках подхода LBYL приводит к двухкратной проверке на наличие ключа в словаре, в то время как подход EAFP позволяет этого избежать.
В-третьих, между проверкой на безопасность операции и началом выполнения операции многое может измениться.
Такая ситуация может случиться в многопоточном приложении. Тут актуален и предыдущий пример со словарем: при подходе LBYL возможна ситуация, когда сразу после проверки на наличие ключа в словаре инструкцией k in d
, другой поток удалит этот ключ, что приведет к возникновению исключения KeyError
. Второй подход лишен такого недостатка, так как такой проверки вовсе не производится.
Так же такая ситуация может возникнуть в приложения, работающих сетью. В таких приложениях принципиально нельзя убедиться, что операцию по сети удаться успешно завершить, т.к. не всё находится под контролем программы: сеть может в любой момент упасть. В таких ситуациях подход LBYL почти не заменим.