Стратегии контроля ошибок

Стратегии контроля ошибок

Как уже упоминалось, возникновение исключений в 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)
Input In [2], in <cell line: 1>()
----> 1 f(2.5)

Input In [1], in f(x)
      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 почти не заменим.