Перейти до змісту

Обробка винятків як керування ходом виконання програми

В статично типізованих мовах програмування компілятор контроює, чи реалізує клас, екземпляром якого є певний об'єкт, певний інтерфейс. При динамічній качиній типізації відповідальність за це лежить на програмісті.

Як же можна з'ясувати, чи реалізує якийсь клас певний інтерфейс (наприклад, метод)? Існує два протилежних підходи до реалізації таких перевірок.

Перший полягає у тому, що ми маємо можливість дійсно перевірити, чи реалізує клас певний інтерфейс, тобто чи має він певний атрибут. Зробити це можна за допомогою функції:

hasattr(object, name)

Функція повертає True якщо object має атрибут з ідентифікатором name і False в інших випадках.

Отже перевірка наявності інтерфейса і подальше його використання можуть виглядати так:

p = Person('John Doe')
if hasattr(p, show_info):
    p.show_info()
else:
    print('No information')

Інший спосіб полягає у тому, що ми не перевіряємо наявність певного інтерфейса, а одразу ж пробуємо його використовувати. Якщо інтерфейс не реалізовано, то, звісна річ, виникне виняткова ситуація. Але ми можемо перехопити виняток і обробити його так як нам потрібно!

Повертаючись до попереднього прикладу:

p = Person('John Doe')
try:
    p.show_info()
except AttributeError:
    print('No information')

Отже ми маємо два різних підходи (стилі) написання коду, вони навіть мають свої назви:

  • Look Before You Leap (LBYL, "Не спитавши броду не лізь у воду"). Стиль, який характеризується наявністю багатьох перевірок і умовних операторів. В контексті качиної типізації може означати перевірку наявності необхідних атрибутів за допомогою функції hasattr.
  • Easier to Ask for Forgiveness than Permission (EAFP, "простіше попросити вибачення, ніж дозволу"). Стиль, який характеризується наявністю блоків try/except. В контексті качиної типізації — написання кода виходячи з припущення, що певний об'єкт реалізує необхідний інтерфейс, і обробка винятка AttributeError в іншому випадку.

LBYL і EAFP — це доволі загальні стилі написання кода на динамічних мовах програмування, які стосуються не тільки качиної типізації. Наприклад:

  • перевірка існування ключа у словнику (LBYL) або обробка винятка KeyError (EAFP)
  • перевірка існування файла (LBYL) або обробка винятка IOError (EAFP)

Давайте напишемо рішення однієї і тої ж задачі використовуючи обидва стилі.

Задача: попросити користувача ввести два цілих числа, кожне окремо, і вивести результат ділення першого на друге. Користувач робить ввод даних поки введені дані не будуть коректними, тобто користувач має ввести саме цілі числа, крім того друге з них не повинно дорівнювати 0.

Спочатку напишемо "звичайним" для нас на даний момент способом:

while True:
    s = input('Enter first integer: ')
    if s.strip().isdigit():
        n1 = int(s)
        break
    else:
        print('Not integer number!')
while True:
    s = input('Enter second integer: ')
    if s.strip().isdigit():
        n2 = int(s)
        if n2 != 0:
            break
        else:
            print('Can not divide by zero!')
    else:
        print('Not integer number!')
print(n1/n2)

Зауважте, що цей код ще зовсім не досконалий: ми, наприклад, не враховуємо, що користувач може вводити від'ємні цілі числа, для цього нам треба обробляти можливий знак мінус. Спробуйте зробити це самостійно і оцінити код який отримаєте.

А тепер давайте вирішимо цю ж саму задачу дещо іншим способом:

while True:
    try:
        n1 = int(input('Enter first integer: '))
        break
    except ValueError:
        print('Not a valid number!')

while True:
    try:
        n2 = int(input('Enter second integer: '))
        print(n1/n2)
        break
    except ValueError:
        print('Not a valid number!')
    except ZeroDivisionError:
        print('Can not divide by zero!')

Тут ми не робимо ніяких перевірок даних, які ввів користувач. Ми просто перетворюємо символьний рядок на ціле число, і якщо нам це не вдалося, то робимо висновок, що введені користувачем дані не є цілим числом. Тепер ми можемо вводити і від'ємні цілі числа, і усе працюватиме як слід!

І, наприклад, якщо ми захочемо, щоб користувач мав змогу вводити не тільки цілі числа, а й дійсні, ми просто будемо перетворювати введені користувачем дані до типа float. Уявіть (а краще спробуйте реалізувати) перевірку введених даних на відповідність дійсним числам без використання обробки винятків і оцініть складність і читабельність такого кода.

Аналогічно відбувається і перевірка — чи є друге введене користувачем число нулем.

Порівняйте другий варіант з першим по читабельності кода.

LBYL vs EAFP

В Python деколи надають перевагу другому стилю — EAFP.

  • код простіше читається дякуючи відсутності зайвих перевірок;
  • винятки в Python працюють відносно швидко;
  • позбавляємось ризику виникнення стану перегонів у багатопоточних середовищах, що деколи трапляється при використанні підхода LBYL.

Однак користуватись винятками і їх обробкою для керуванням ходу виконання програми треба обережно і вдумливо, якщо це дійсно є виправданим. Необережне і невміле використання цього методу може призвести до серйозних помилок в реалізації алгоритму які, зазвичай, ще й важко виявити. Не зловживайте EAFP і по можливості уникайте.

Back to top