Практика · Тема 18 · Помилки та винятки¶
Лекція: lecture.html · Тест: quiz.html · Домашнє завдання: homework.md
Наскрізний приклад той самий, що в лекції, — битий текстовий прайс. Кілька рядків у ньому зіпсовані, і зіпсовані по-різному. Наша мета: не впасти на першому ж із них.
Що зробимо руками:
- подивимось на справжній traceback, коли обробки немає взагалі;
- напишемо
try/exceptна конкретний тип і переконаємось, що вузька гілка ловить те, що назвала; - побачимо на власні очі, як голий
exceptковтає одрук програміста; - перевіримо порядок гілок і знайдемо недосяжну;
- доведемо через
assert, щоfinallyвиконався навіть тоді, коли виняток полетів далі; - піднімемо виняток самі — з
raiseі зraise ... from; - зробимо власний тип
ПомилкаПрайсуза рецептом із лекції; - виміряємо, скільки насправді коштує виняток, і порівняємо EAFP з LBYL.
1 · Дані, з якими працюємо¶
Прайс прийшов текстом: кожен рядок — «назва;ціна». Два рядки з пʼяти зіпсовані, і зіпсовані по-різному — це нам і потрібно.
рядки_прайсу = [
"хліб;28.5",
"молоко;32.0",
"яблука;девʼятнадцять", # ціна словами — float() на цьому спіткнеться
"мед", # крапки з комою немає — другого елемента не буде
"сіль;12.0",
]
print("рядків у прайсі:", len(рядки_прайсу))
for номер, рядок in enumerate(рядки_прайсу, start=1):
print(f"{номер}. {рядок}")
рядків у прайсі: 5 1. хліб;28.5 2. молоко;32.0 3. яблука;девʼятнадцять 4. мед 5. сіль;12.0
2 · Спершу — без жодної обробки¶
Напишемо найпростішу функцію розбору й запустимо її на всьому прайсі. Вона впаде. Це нормально: traceback тут — навчальний матеріал, а не невдача.
Наступна клітинка навмисно падає. Прочитай її вивід з кінця: останній рядок — діагноз, рядки вище — маршрут, яким виняток летів по стеку (про це був інтерактив 5 у лекції).
def розібрати(рядок):
"""Розкладає "назва;ціна" на пару (назва, ціна). Поки що без жодного захисту."""
частини = рядок.split(";")
return частини[0], float(частини[1])
# перший рядок розбирається без пригод
print("перший рядок:", розібрати(рядки_прайсу[0]))
перший рядок: ('хліб', 28.5)
# а на третьому все зупиниться — навмисна помилка
підсумок = []
for рядок in рядки_прайсу:
підсумок.append(розібрати(рядок))
print("сюди виконання не дійде:", підсумок)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In[3], line 4 2 підсумок = [] 3 for рядок in рядки_прайсу: ----> 4 підсумок.append(розібрати(рядок)) 5 print("сюди виконання не дійде:", підсумок) Cell In[2], line 4, in розібрати(рядок) 2 """Розкладає "назва;ціна" на пару (назва, ціна). Поки що без жодного захисту.""" 3 частини = рядок.split(";") ----> 4 return частини[0], float(частини[1]) ValueError: could not convert string to float: 'девʼятнадцять'
Зверни увагу: у списку підсумок встигли осісти два рядки, а решту втрачено.
Виняток обірвав цикл на третьому елементі — і ніхто не дізнався б, що четвертий
і пʼятий узагалі існують.
print("у підсумку опинилось:", підсумок)
print("а рядків у прайсі було:", len(рядки_прайсу))
print("втрачено рядків:", len(рядки_прайсу) - len(підсумок))
у підсумку опинилось: [('хліб', 28.5), ('молоко', 32.0)]
а рядків у прайсі було: 5
втрачено рядків: 3
3 · try / except на конкретний тип¶
Тепер обгорнемо ризиковану дію. Ловимо саме ValueError — той тип, який кидає float,
коли рядок не схожий на число.
Дивись, де стоїть try: навколо виклику всередині циклу, а не навколо всього циклу.
Саме тому цикл переживе поганий рядок і піде далі.
розібрані = []
пропущені = []
for рядок in рядки_прайсу:
try:
розібрані.append(розібрати(рядок))
except ValueError:
# тільки «ціна не число» — усі інші біди летять далі, як і має бути
пропущені.append(рядок)
print("розібрано:", розібрані)
print("пропущено:", пропущені)
--------------------------------------------------------------------------- IndexError Traceback (most recent call last) Cell In[5], line 6 4 for рядок in рядки_прайсу: 5 try: ----> 6 розібрані.append(розібрати(рядок)) 7 except ValueError: 8 # тільки «ціна не число» — усі інші біди летять далі, як і має бути 9 пропущені.append(рядок) Cell In[2], line 4, in розібрати(рядок) 2 """Розкладає "назва;ціна" на пару (назва, ціна). Поки що без жодного захисту.""" 3 частини = рядок.split(";") ----> 4 return частини[0], float(частини[1]) IndexError: list index out of range
Знову падіння — і це найкорисніший момент усієї практики. Ми впіймали ValueError,
але рядок "мед" кидає IndexError: після split(";") у списку один елемент,
а ми просимо другий. Вузька гілка чесно пропустила чужий тип далі.
Перевіримо це прямо, без циклу:
# два різні рядки — два різні типи винятку
for поганий in ["яблука;девʼятнадцять", "мед"]:
try:
розібрати(поганий)
except BaseException as e: # тут ловимо широко НАВМИСНО — щоб показати тип
print(f"{поганий!r:<28} → {type(e).__name__}: {e}")
'яблука;девʼятнадцять' → ValueError: could not convert string to float: 'девʼятнадцять' 'мед' → IndexError: list index out of range
4 · Дві гілки на дві біди¶
Реакція на різні біди може бути різною — тоді пишемо кілька except.
Python перебирає їх згори вниз і виконує рівно одну: першу, яка підходить.
розібрані = []
журнал = []
for рядок in рядки_прайсу:
try:
розібрані.append(розібрати(рядок))
except ValueError:
журнал.append(f"{рядок!r}: ціна не число")
except IndexError:
журнал.append(f"{рядок!r}: немає крапки з комою")
print("розібрано позицій:", len(розібрані))
for пара in розібрані:
print(" ", пара)
print("\nжурнал проблем:")
for запис in журнал:
print(" ", запис)
# перевіряємо, що жоден рядок не загубився: кожен або розібрано, або записано в журнал
assert len(розібрані) + len(журнал) == len(рядки_прайсу), "рядки десь загубились!"
print("\n✅ усі 5 рядків обліковано: 3 розібрано + 2 у журналі")
розібрано позицій: 3
('хліб', 28.5)
('молоко', 32.0)
('сіль', 12.0)
журнал проблем:
'яблука;девʼятнадцять': ціна не число
'мед': немає крапки з комою
✅ усі 5 рядків обліковано: 3 розібрано + 2 у журналі
Якщо реакція на обидві біди однакова, гілки зливають в одну через кортеж типів. Дужки обовʼязкові. Результат має бути точнісінько той самий — це ми зараз і доведемо.
розібрані_2 = []
пропущені_2 = []
for рядок in рядки_прайсу:
try:
розібрані_2.append(розібрати(рядок))
except (ValueError, IndexError):
# одна реакція на дві різні біди — кортеж типів у дужках
пропущені_2.append(рядок)
print("розібрано:", розібрані_2)
print("пропущено:", пропущені_2)
# та сама відповідь, що й у версії з двома гілками
assert розібрані_2 == розібрані, "кортеж типів дав інший результат!"
print("✅ кортеж типів дає рівно те саме, що дві окремі гілки")
розібрано: [('хліб', 28.5), ('молоко', 32.0), ('сіль', 12.0)]
пропущено: ['яблука;девʼятнадцять', 'мед']
✅ кортеж типів дає рівно те саме, що дві окремі гілки
5 · Чим небезпечний голий except¶
А тепер найважливіша демонстрація теми. Зробимо в коді одрук: напишемо flot
замість float. Це NameError — помилка програміста, а не погані дані.
Спершу подивимось, як поводиться голий except.
def розібрати_голий(рядок):
"""Та сама функція, але з одруком у коді й з голим except."""
try:
частини = рядок.split(";")
return частини[0], flot(частини[1]) # ← одрук: flot замість float
except: # ← ловить геть усе
return рядок, 0.0
результат_голий = [розібрати_голий(р) for р in рядки_прайсу]
for пара in результат_голий:
print(пара)
('хліб;28.5', 0.0)
('молоко;32.0', 0.0)
('яблука;девʼятнадцять', 0.0)
('мед', 0.0)
('сіль;12.0', 0.0)
Подивись уважно на вивід: жодної помилки, жодного попередження. Програма відпрацювала, повернула пʼять пар, усі ціни дорівнюють нулю. Звіт зійдеться, сума порахується, і ніхто ніколи не дізнається, що в коді одрук.
Доведімо це числом: правильна сума прайсу — 72.5 (28.5 + 32.0 + 12.0),
а голий except дав нуль.
сума_голого = sum(ціна for _, ціна in результат_голий)
правильна_сума = 28.5 + 32.0 + 12.0
print("сума за версією з голим except:", сума_голого)
print("правильна сума трьох цілих рядків:", правильна_сума)
# бага не видно ніде, крім самого числа — саме це й робить його найдорожчим
assert сума_голого == 0.0, "очікували, що голий except підставить нулі"
assert сума_голого != правильна_сума, "числа мали розійтись"
print("\n❌ програма не впала, але порахувала неправильно — і мовчить про це")
сума за версією з голим except: 0.0 правильна сума трьох цілих рядків: 72.5 ❌ програма не впала, але порахувала неправильно — і мовчить про це
Тепер той самий одрук, але з конкретним except ValueError. Наступна клітинка
навмисно падає — і це саме те, чого ми хотіли: помилку в коді видно одразу,
з іменем, номером рядка й типом.
def розібрати_конкретний(рядок):
"""Той самий одрук, але ловимо лише ValueError."""
try:
частини = рядок.split(";")
return частини[0], flot(частини[1]) # ← той самий одрук
except ValueError:
return рядок, 0.0
розібрати_конкретний("хліб;28.5")
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[11], line 10 6 except ValueError: 7 return рядок, 0.0 ---> 10 розібрати_конкретний("хліб;28.5") Cell In[11], line 5, in розібрати_конкретний(рядок) 3 try: 4 частини = рядок.split(";") ----> 5 return частини[0], flot(частини[1]) # ← той самий одрук 6 except ValueError: 7 return рядок, 0.0 NameError: name 'flot' is not defined
Той самий одрук — два зовсім різні наслідки. Різниця в одному слові після except.
Ще одна річ, яку ковтає голий except, — це KeyboardInterrupt (Ctrl+C).
У зошиті натиснути Ctrl+C складно, тому змоделюємо: кинемо KeyboardInterrupt
самі й подивимось, чи проходить він крізь обидві версії.
def ковтає_все():
try:
raise KeyboardInterrupt # так виглядає натиснутий Ctrl+C зсередини
except:
return "голий except: Ctrl+C проковтнуто, програма працює далі"
def пропускає_ctrl_c():
try:
raise KeyboardInterrupt
except Exception: # KeyboardInterrupt НЕ є підтипом Exception
return "сюди ми не потрапимо"
print(ковтає_все())
# перевіряємо, що except Exception справді пропускає KeyboardInterrupt повз себе
try:
пропускає_ctrl_c()
зупинилось = False
except KeyboardInterrupt:
зупинилось = True
assert зупинилось, "except Exception не мав ловити KeyboardInterrupt!"
print("except Exception: Ctrl+C пролетів далі — програму можна зупинити ✅")
голий except: Ctrl+C проковтнуто, програма працює далі except Exception: Ctrl+C пролетів далі — програму можна зупинити ✅
6 · Порядок гілок вирішує все¶
Гілки перебираються згори вниз. Якщо широкий тип поставити раніше за вузький, вузький не спрацює ніколи — і Python про це не скаржиться.
Порахуємо це експериментом: проженемо три різні винятки через дві версії коду й порівняємо, скільки разів спрацювала точна гілка.
def версія_A(що_кинути):
"""Широкий тип першим — класична пастка."""
try:
raise що_кинути
except Exception:
return "загальне"
except ValueError:
return "точне" # цей рядок недосяжний
def версія_B(що_кинути):
"""Вузький тип першим — так і треба."""
try:
raise що_кинути
except ValueError:
return "точне"
except Exception:
return "загальне"
винятки = [ValueError("не число"), KeyError("мед"), ZeroDivisionError("ділення на нуль")]
for виняток in винятки:
імʼя = type(виняток).__name__
print(f"{імʼя:<20} A → {версія_A(виняток):<9} B → {версія_B(виняток)}")
точних_у_A = sum(1 for в in винятки if версія_A(в) == "точне")
точних_у_B = sum(1 for в in винятки if версія_B(в) == "точне")
print(f"\nточна гілка спрацювала: у версії A — {точних_у_A} раз(и), у версії B — {точних_у_B}")
assert точних_у_A == 0, "у версії A точна гілка мала бути недосяжною"
assert точних_у_B == 1, "у версії B точна гілка мала спрацювати рівно раз"
print("✅ підтверджено: широкий except вище робить вузький мертвим кодом")
ValueError A → загальне B → точне KeyError A → загальне B → загальне ZeroDivisionError A → загальне B → загальне точна гілка спрацювала: у версії A — 0 раз(и), у версії B — 1 ✅ підтверджено: широкий except вище робить вузький мертвим кодом
7 · else і finally: доводимо, що finally виконується завжди¶
Найважливіша обіцянка finally — «виконаюсь у будь-якому разі». Перевіримо її
не на слово, а assert-ом. Заведемо список-слід: кожен блок дописуватиме туди
своє імʼя, і ми потім подивимось, що там опинилось.
слід = []
def прочитати_ціну(текст):
"""Повертає ціну; лишає в списку `слід` запис про кожен блок, який відпрацював."""
try:
ціна = float(текст)
except ValueError:
слід.append("except")
ціна = 0.0
else:
# виконується ЛИШЕ якщо в try не було винятку
слід.append("else")
finally:
# виконується завжди — і після успіху, і після впійманого винятку
слід.append("finally")
return ціна
слід.clear()
print("нормальний рядок →", прочитати_ціну("28.5"), " слід:", слід)
слід.clear()
print("битий рядок →", прочитати_ціну("девʼятнадцять"), " слід:", слід)
нормальний рядок → 28.5 слід: ['else', 'finally'] битий рядок → 0.0 слід: ['except', 'finally']
Тепер найцікавіший випадок: виняток, якого except не ловить. Він полетить
угору по стеку — а finally усе одно має відпрацювати перед його відльотом.
float(None) кидає TypeError, а не ValueError — саме те, що нам потрібно.
слід.clear()
полетів_далі = None
try:
прочитати_ціну(None) # float(None) → TypeError, наш except його не ловить
except TypeError as e:
полетів_далі = str(e)
print("слід після неспійманого винятку:", слід)
print("виняток таки долетів до нас:", полетів_далі)
# головна перевірка теми: finally відпрацював, хоча функція завершилась винятком
assert "finally" in слід, "finally НЕ виконався — а мусив!"
assert "except" not in слід, "except не мав спрацювати на TypeError"
assert "else" not in слід, "else не виконується, коли був виняток"
assert полетів_далі is not None, "TypeError мав долетіти до зовнішнього try"
print("\n✅ finally виконався навіть тоді, коли виняток полетів далі по стеку")
слід після неспійманого винятку: ['finally'] виняток таки долетів до нас: float() argument must be a string or a real number, not 'NoneType' ✅ finally виконався навіть тоді, коли виняток полетів далі по стеку
І окремо — пастка, про яку попереджала лекція: return усередині finally
перебиває виняток, який летів угору. Виняток просто зникає.
Дивись, як тихо це відбувається.
def підступна():
try:
raise ValueError("я мав долетіти нагору")
finally:
return "значення з finally" # ← виняток мовчки зникає тут
результат = підступна()
print("функція повернула:", результат)
print("а виняток? його немає — його проковтнув return у finally")
assert результат == "значення з finally"
print("\n⚠️ саме тому у finally прибирають, але не повертають")
функція повернула: значення з finally а виняток? його немає — його проковтнув return у finally ⚠️ саме тому у finally прибирають, але не повертають
8 · raise: піднімаємо виняток самі¶
Замість того щоб чекати на незрозумілий IndexError десь усередині, чесніше
перевірити вхід самому й кинути виняток зі змістовним текстом. Це та сама
«рання відмова» з теми 14.
def розібрати_суворо(рядок):
"""Розкладає "назва;ціна". Кидає ValueError зі зрозумілим поясненням."""
частини = рядок.split(";")
if len(частини) != 2:
raise ValueError(f"очікував дві частини через ';', а маю {len(частини)}: {рядок!r}")
назва, текст_ціни = частини
if not назва.strip():
raise ValueError(f"порожня назва в рядку {рядок!r}")
return назва, float(текст_ціни)
print("нормальний рядок:", розібрати_суворо("хліб;28.5"))
for поганий in ["мед", ";50.0"]:
try:
розібрати_суворо(поганий)
except ValueError as e:
print(f"{поганий!r:<10} → ValueError: {e}")
нормальний рядок: ('хліб', 28.5)
'мед' → ValueError: очікував дві частини через ';', а маю 1: 'мед'
';50.0' → ValueError: порожня назва в рядку ';50.0'
Обʼєкт винятку зблизька¶
Виняток — звичайний обʼєкт. Через as e до нього можна дотягнутись і подивитись,
що всередині: тип, текст повідомлення, кортеж аргументів.
try:
float("девʼятнадцять")
except ValueError as e:
print("тип :", type(e).__name__)
print("str(e) :", str(e))
print("e.args :", e.args)
print("аргументів :", len(e.args))
текст_помилки = str(e) # переписуємо в іншу змінну: імʼя e живе лише в блоці
print("\nпісля блоку except імʼя 'e' уже не існує, а копія тексту лишилась:")
print(" ", текст_помилки)
assert "девʼятнадцять" in текст_помилки, "у повідомленні мав бути сам зіпсований рядок"
print("\n✅ повідомлення винятку містить те саме значення, яке зламало float()")
тип : ValueError
str(e) : could not convert string to float: 'девʼятнадцять'
e.args : ("could not convert string to float: 'девʼятнадцять'",)
аргументів : 1
після блоку except імʼя 'e' уже не існує, а копія тексту лишилась:
could not convert string to float: 'девʼятнадцять'
✅ повідомлення винятку містить те саме значення, яке зламало float()
Ланцюжок: raise ... from e¶
Часто виняток піднімають усередині except, щоб перевести технічну біду
в поняття своєї задачі. Слово from каже: «ось первинна причина».
Python зберігає її в атрибуті __cause__.
def ціна_або_помилка(рядок):
"""Переводить будь-яку біду розбору в один зрозумілий ValueError із причиною."""
try:
return розібрати_суворо(рядок)[1]
except ValueError as e:
raise ValueError(f"рядок прайсу непридатний: {рядок!r}") from e
try:
ціна_або_помилка("яблука;девʼятнадцять")
except ValueError as e:
print("наш виняток :", e)
print("__cause__ :", repr(e.__cause__))
причина = e.__cause__
# from e не втрачає первинної інформації — вона лежить поруч, у __cause__
assert причина is not None, "причина мала зберегтись у __cause__"
assert isinstance(причина, ValueError)
print("\n✅ первинна причина збережена й доступна програмно, не лише очима в traceback")
наш виняток : рядок прайсу непридатний: 'яблука;девʼятнадцять'
__cause__ : ValueError("could not convert string to float: 'девʼятнадцять'")
✅ первинна причина збережена й доступна програмно, не лише очима в traceback
9 · Власний виняток¶
Рецепт із лекції: один рядок class, успадкування від Exception, у тілі — докстрінг.
Детально про класи буде в темі 25; тут це просто спосіб дати своїй біді власне імʼя.
class ПомилкаПрайсу(Exception):
"""Рядок прайсу не вдалося розібрати."""
def розібрати_з_власним(рядок):
"""Будь-яку біду розбору перетворює на ПомилкаПрайсу — з причиною всередині."""
try:
return розібрати_суворо(рядок)
except ValueError as e:
raise ПомилкаПрайсу(f"не можу розібрати {рядок!r}") from e
try:
розібрати_з_власним("мед")
except ПомилкаПрайсу as e:
print("зловили власний тип:", type(e).__name__)
print("текст :", e)
# власний тип — усе ще підтип Exception, тому загальні обробники його теж бачать
assert issubclass(ПомилкаПрайсу, Exception)
assert not issubclass(ПомилкаПрайсу, KeyboardInterrupt)
print("\nПомилкаПрайсу є підтипом Exception:", issubclass(ПомилкаПрайсу, Exception))
print("✅ значить, чужий except Exception її зловить, а Ctrl+C вона не зачепить")
зловили власний тип: ПомилкаПрайсу текст : не можу розібрати 'мед' ПомилкаПрайсу є підтипом Exception: True ✅ значить, чужий except Exception її зловить, а Ctrl+C вона не зачепить
Перевага власного типу — можливість відрізнити «мої дані погані» від «усередині щось зламалось». Порівняй дві гілки:
def обробити_прайс(рядки):
"""Повертає (список цін, список скарг). Свою біду відрізняє від чужої."""
ціни, скарги = [], []
for рядок in рядки:
try:
назва, ціна = розібрати_з_власним(рядок)
except ПомилкаПрайсу as e:
скарги.append(str(e)) # очікувана біда: погані дані
else:
ціни.append((назва, ціна))
return ціни, скарги
ціни, скарги = обробити_прайс(рядки_прайсу)
print("розібрано:")
for пара in ціни:
print(" ", пара)
print("скарги:")
for скарга in скарги:
print(" ", скарга)
сума = sum(ціна for _, ціна in ціни)
print("\nсума придатних позицій:", round(сума, 2))
assert round(сума, 2) == 72.5, "сума трьох цілих рядків має бути 72.5"
assert len(скарги) == 2, "зіпсованих рядків було рівно два"
print("✅ прайс оброблено повністю: 3 позиції на 72.5, 2 скарги")
розібрано:
('хліб', 28.5)
('молоко', 32.0)
('сіль', 12.0)
скарги:
не можу розібрати 'яблука;девʼятнадцять'
не можу розібрати 'мед'
сума придатних позицій: 72.5
✅ прайс оброблено повністю: 3 позиції на 72.5, 2 скарги
10 · EAFP проти LBYL¶
Одне завдання — взяти ціну зі словника, з нулем як запасним варіантом — двома способами. Спершу переконаємось, що відповідь однакова, потім поміряємо, скільки це коштує.
ціни_словник = {"хліб": 28.5, "молоко": 32.0, "сіль": 12.0}
def через_lbyl(словник, ключ):
"""Look Before You Leap: спершу перевір, потім бери."""
if ключ in словник:
return словник[ключ]
return 0.0
def через_eafp(словник, ключ):
"""Easier to Ask Forgiveness than Permission: бери, а як не вийде — обробляй."""
try:
return словник[ключ]
except KeyError:
return 0.0
for ключ in ["хліб", "мед"]:
a, b = через_lbyl(ціни_словник, ключ), через_eafp(ціни_словник, ключ)
print(f"{ключ:<8} LBYL → {a:<6} EAFP → {b}")
assert a == b, "два підходи мали дати однакову відповідь!"
print("\n✅ на однакових даних обидва підходи дають однаковий результат")
хліб LBYL → 28.5 EAFP → 28.5 мед LBYL → 0.0 EAFP → 0.0 ✅ на однакових даних обидва підходи дають однаковий результат
Тепер про ціну. Виміряємо два випадки окремо: коли ключ є (винятку немає) і коли ключа немає (виняток кидається й ловиться на кожній ітерації).
Числа на різних машинах будуть різні — важливе співвідношення, а не абсолют.
import time
ПОВТОРІВ = 100_000
def поміряти(функція, ключ):
"""Скільки мікросекунд у середньому займає один виклик."""
початок = time.perf_counter()
for _ in range(ПОВТОРІВ):
функція(ціни_словник, ключ)
минуло = time.perf_counter() - початок
return минуло / ПОВТОРІВ * 1_000_000 # мікросекунди на один виклик
є_lbyl = поміряти(через_lbyl, "хліб")
є_eafp = поміряти(через_eafp, "хліб")
нема_lbyl = поміряти(через_lbyl, "мед")
нема_eafp = поміряти(через_eafp, "мед")
print(f"{'':<22}{'LBYL':>10}{'EAFP':>10} (мкс на виклик)")
print(f"{'ключ Є':<22}{є_lbyl:>10.3f}{є_eafp:>10.3f}")
print(f"{'ключа НЕМАЄ':<22}{нема_lbyl:>10.3f}{нема_eafp:>10.3f}")
# у кожне число входить іще й вартість самого виклику функції — щоб її прибрати,
# беремо РІЗНИЦЮ між двома вимірами на однакових промахах
надлишок = нема_eafp - нема_lbyl
print(f"\nна промахах EAFP повільніший у {нема_eafp / нема_lbyl:.1f} раза")
print(f"чистий надлишок одного кинутого й спійманого винятку: {надлишок:.2f} мкс")
print("а коли ключ є, EAFP швидший: він робить одне звернення замість двох")
LBYL EAFP (мкс на виклик) ключ Є 0.137 0.107 ключа НЕМАЄ 0.089 0.526 на промахах EAFP повільніший у 5.9 раза чистий надлишок одного кинутого й спійманого винятку: 0.44 мкс а коли ключ є, EAFP швидший: він робить одне звернення замість двох
Висновок з чисел вище: EAFP програє лише тоді, коли промахи часті. Якщо ключ знаходиться майже завжди, зайва перевірка коштує дорожче за рідкісний виняток.
Зверни увагу на «чистий надлишок»: у ньому вже немає вартості виклику функції, і саме він порівнюється зі звичайним зверненням до словника (десятки наносекунд). Звідси й орієнтир із лекції — виняток дорожчий за перевірку приблизно в 15-30 разів.
І окремо — інструмент, який знімає питання зовсім: метод get зі словника
(тема 08). Одне звернення, жодного винятку.
print("через get:", ціни_словник.get("мед", 0.0))
# усі три способи дають те саме — просто get найкоротший і найшвидший
assert ціни_словник.get("мед", 0.0) == через_lbyl(ціни_словник, "мед") == через_eafp(ціни_словник, "мед")
print("✅ get, LBYL і EAFP дали однакову відповідь — коли є готовий інструмент, бери його")
через get: 0.0 ✅ get, LBYL і EAFP дали однакову відповідь — коли є готовий інструмент, бери його
11 · contextlib.suppress — чесний спосіб проігнорувати¶
Якщо виняток справді очікуваний і нецікавий, є конструкція, яка каже це вголос —
на відміну від except: pass, який мовчить про все на світі.
from contextlib import suppress
значення = {"а": 1, "б": 2}
# ігноруємо ТІЛЬКИ відсутність ключа; будь-яка інша біда полетить далі
with suppress(KeyError):
del значення["в"] # такого ключа немає — і це нормально
print("словник після suppress:", значення)
# доводимо, що suppress не ковтає чужі типи
спіймали_чуже = False
try:
with suppress(KeyError):
значення["а"] + "текст" # TypeError — suppress його не стосується
except TypeError:
спіймали_чуже = True
assert спіймали_чуже, "suppress(KeyError) не мав ковтати TypeError"
print("✅ suppress пропустив TypeError далі — на відміну від голого except")
словник після suppress: {'а': 1, 'б': 2}
✅ suppress пропустив TypeError далі — на відміну від голого except
Завдання¶
🟢 Рівень 1 — База¶
Напиши функцію безпечне_ділення(a, b), яка повертає a / b, а при діленні на нуль
повертає рядок "на нуль ділити не можна". Ловити треба конкретний тип.
Перевір себе:
assert безпечне_ділення(10, 2) == 5.0
assert безпечне_ділення(10, 0) == "на нуль ділити не можна"
assert безпечне_ділення(0, 5) == 0.0
print("✅ рівень 1")
Зроблено, якщо: усі три assert мовчать, а у функції написано except ZeroDivisionError,
а не голий except і не except Exception.
🟡 Рівень 2 — Плюс¶
Візьми функцію обробити_прайс із розділу 9 і додай до неї третій список — аварії,
куди потрапляють несподівані винятки (усе, що не ПомилкаПрайсу). Потім:
- передай їй список, у якому один елемент — не рядок, а число
42(на ньому.splitкинеAttributeError); - переконайся
assert-ом, що число опинилось саме ваварії, а зіпсовані рядки — ускарги; - додай
finally, який дописує в лічильник кількість оброблених рядків, і доведиassert-ом, що лічильник дорівнює довжині вхідного списку.
Зроблено, якщо: у зошиті є три assert — на скарги, на аварії й на лічильник, —
і всі три мовчать.
🔴 Рівень 3 — Виклик¶
Побудуй маленьку ієрархію власних винятків і доведи, що вона працює як дерево:
ПомилкаПрайсу(Exception)
├── ПомилкаЦіни — ціна не число або відʼємна
└── ПомилкаФормату — рядок не розкладається на дві частини
Далі:
- Перепиши
розібрати_суворотак, щоб вона кидала точний підтип у кожному випадку, зберігаючи первинну причину черезfrom e. - Напиши три обробники й доведи
assert-ами, що:except ПомилкаЦіниловить помилку ціни й не ловить помилку формату;except ПомилкаПрайсуловить обидві;except Exceptionтеж ловить обидві, аexcept KeyboardInterrupt— жодної.
- Виміряй, скільки рядків traceback дає варіант
from eі варіантfrom Noneна одному й тому самому зіпсованому рядку. Підказка:traceback.format_exc()повертає traceback рядком, аlen(...splitlines())рахує рядки.
Зроблено, якщо: усі assert мовчать, а в зошиті надруковані два числа —
довжина traceback із from e і без нього — і вони різні.