Към съдържанието
wannacode

Различни грешки и finally

Проблем

Сметката за нула души

Програмата дели сметката в ресторанта по равно. Вече знаеш за грешния вход и го хващаш:

bill.py
try:
bill = int(input())
people = int(input())
print(f"На човек: {bill // people} €")
except ValueError:
print("Въведи цели числа.")

Сметката е 90 €, а някой въвежда 0 за броя на хората. Програмата пак спира:

Грешка
Traceback (most recent call last):
File "/projects/bill/bill.py", line 4, in <module>
print(f"На човек: {bill // people} €")
~~~~~^^~~~~~~~
ZeroDivisionError: integer division or modulo by zero

except е там, но не помага. Хваща само ValueError, а това е друг вид изключение.

Идеята

Няколко except

След един try може да има няколко except, по един за всеки вид грешка:

try:
bill = int(input())
people = int(input())
print(f"На човек: {bill // people} €")
except ValueError:
print("Въведи цели числа.")
except ZeroDivisionError:
print("Хората не може да са 0.")
Вход Изход
90 и 3 На човек: 30 €
90 и 0 Хората не може да са 0.
90 и трима Въведи цели числа.

Блоковете се проверяват отгоре надолу. Изпълнява се първият, чийто вид отговаря, и само той. Останалите се прескачат.

try:
bill = int(input())
people = int(input())
print(f"На човек: {bill // people} €")
except ValueError:
print("Въведи цели числа.")
except ZeroDivisionError:
print("Хората не може да са 0.")
Стъпка по стъпка, с думи
  1. Първо пускане: сметка 90 €, хората са 0.
  2. 90 / 0 е деление на нула: хвърля се ZeroDivisionError.
  3. Първият except е за ValueError. Не пасва, прескача се.
  4. Вторият пасва. Изпълнява се той и само той.
  5. Второ пускане: вместо брой хора е написано „трима“.
  6. int() не може да направи число: хвърля се ValueError.
  7. Пасва още първият except. До втория не се стига.
  8. Блоковете се проверяват отгоре надолу. Изпълнява се първият, който пасва.
По-точно, за любопитните
  • Когато два вида се обработват еднакво, можеш да ги хванеш в един блок с кортеж: except (ValueError, ZeroDivisionError):.

Идеята

Семейството на изключенията

Изключенията са класове, свързани с наследяване. Ето най-честите:

Йерархията
BaseException
├── KeyboardInterrupt Ctrl+C, не се хваща
├── SystemExit sys.exit(), не се хваща
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── ValueError
├── TypeError
└── OSError файлове, мрежа
└── FileNotFoundError

except хваща посочения вид и всичките му наследници. Това е полиморфизмът от модула за наследяването: обект на наследника е и от вида на родителя. issubclass го показва:

print(issubclass(ZeroDivisionError, ArithmeticError))
print(issubclass(IndexError, LookupError))
print(issubclass(ValueError, Exception))
print(issubclass(KeyboardInterrupt, Exception))
Изход
True
True
True
False

Затова except Exception as e: хваща почти всичко:

def split(bill_text, people_text):
try:
bill = int(bill_text)
people = int(people_text)
print(f"На човек: {bill // people} €")
except Exception as e:
print(f"{type(e).__name__}: {e}")
split("90", "3")
split("90", "0")
split("90", "трима")
Изход
На човек: 30 €
ZeroDivisionError: integer division or modulo by zero
ValueError: invalid literal for int() with base 10: 'трима'

type(e).__name__ дава името на истинския клас на обекта. except е за Exception, но обектът е от наследник.

По-точно, за любопитните
  • KeyboardInterrupt (Ctrl+C) и SystemExit (sys.exit()) не са наследници на Exception, а на BaseException. Така except Exception: не пречи на програмата да бъде спряна. except: без вид и except BaseException: хващат и тях. Не ги пиши.
  • Хващай толкова конкретно, колкото можеш. except Exception: скрива и грешки, които не си очаквал, например бъг в собствения ти код.

Пробвай

Кой catch?

Какво ще отпечата програмата?

seats = [4, 2, 6]
try:
print(f"Маса 4: {seats[3]} места")
except ZeroDivisionError:
print("Деление на нула")
except LookupError:
print("Грешка при търсене")
except Exception:
print("Обща грешка")
Отговор

Пробвай

Намери грешката

При text = "0" програмата печата „Нещо се обърка.“, а не „Хората не може да са 0.“. На кой ред е грешката?

Избери реда с грешката.

Идеята

Никой не задължава

В Java има проверени изключения: компилаторът не приема програмата, докато не ги хванеш или не ги обявиш с throws. В Python такива няма. Нито едно изключение не е задължително за хващане:

import time
def print_receipt():
print("Печатам бележката...")
time.sleep(0.3)
print("Готово.")
print_receipt()
Изход
Печатам бележката...
Готово.

import time взима модула за времето, а time.sleep(0.3) спира програмата за 0,3 секунди. Ако по време на паузата натиснеш Ctrl+C, Python хвърля KeyboardInterrupt, но никой не иска да го хванеш. Същото е за int() и ValueError.

Затова отговорността е твоя. Кои изключения хвърля една функция, пише в документацията ѝ, обикновено с думата „Raises“. Прочети я, преди да решиш дали да хващаш.

Идеята

finally: винаги

Принтерът на касата трябва да се освободи след всяка бележка, иначе следващата няма да се отпечата. Каквото и да стане, успех или грешка:

def print_receipt(qty_text):
print("Принтерът е зает.")
try:
qty = int(qty_text)
print(f"Кафе: {qty} x 2 € = {qty * 2} €")
except ValueError:
print(f"Грешно количество: {qty_text}")
finally:
print("Принтерът е свободен.")
print_receipt("3")
print_receipt("три")
Изход
Принтерът е зает.
Кафе: 3 x 2 € = 6 €
Принтерът е свободен.
Принтерът е зает.
Грешно количество: три
Принтерът е свободен.

finally се изпълнява винаги: когато всичко мине, когато изключението е хванато и дори когато нито един от тези except не го хваща. В последния случай finally се изпълнява, а изключението продължава нагоре към следващия try, който може да го хване.

Защо не просто ред след try/except? Защото той се пропуска, ако изключението е от вид, който except не хваща, или ако в try има return. finally не се пропуска.

В Python има и else: той се изпълнява само ако в try не е хвърлено изключение. Така в try остава само редът, който може да хвърли, а печатането е след него:

def print_receipt(qty_text):
print("Принтерът е зает.")
try:
qty = int(qty_text)
except ValueError:
print(f"Грешно количество: {qty_text}")
else:
print(f"Кафе: {qty} x 2 € = {qty * 2} €")
finally:
print("Принтерът е свободен.")
print_receipt("3")
print_receipt("три")
Изход
Принтерът е зает.
Кафе: 3 x 2 € = 6 €
Принтерът е свободен.
Принтерът е зает.
Грешно количество: три
Принтерът е свободен.
По-точно, за любопитните
  • Може и try с finally, без except: изключението не се хваща, но почистването се изпълнява.
  • За ресурси, които трябва да се затварят, като файлове и връзки с база данни, има по-кратък запис: with. Ще го видим, когато стигнем до файловете. Той сам затваря ресурса, както би го затворил finally.
  • finally се изпълнява дори при sys.exit(): в Python и това е изключение (SystemExit), което минава през finally нагоре.

Пробвай

finally и return

Какво ще отпечата програмата?

def reserve(guests):
try:
if guests > 6:
return f"Няма маса за {guests}"
return f"Маса за {guests}"
finally:
print("Записах опита.")
print(reserve(8))
Отговор

Упражнение

Програми с няколко except

Реши задачите в редактора си. Не печатай въпроси.

1. Цена на човек. Цените са в списък [120, 75, 300]. Прочети номер на продукт (от 1) и броя на хората и отпечатай цената на човек с целочислено деление. Хвани три неща: несъществуващ продукт, нула души и всичко останало (тогава отпечатай съобщението на изключението).

Вход Изход
2 и 3 На човек: 25 €
5 и 2 Няма такъв продукт.
1 и 0 Броят на хората не може да е 0.
1 и двама Грешка: invalid literal for int() with base 10: 'двама'

2. Калкулатор. Прочети цяло число, оператор (+, -, * или /) и второ цяло число, всяко на свой ред. Отпечатай сметката (делението е //). При текст вместо число кажи „Въведи цели числа.“, при деление на нула „Деление на нула.“, а при друг оператор „Непознат оператор: “ и оператора. За оператора е удобен match.

Вход
84
/
4
Изход
84 / 4 = 21
Вход
84
/
0
Изход
Деление на нула.

Още задачи

  • Терминалът. Функция pay(amount_text) печата „Терминалът е свързан.“, после „Платени X €“ или съобщение за грешен вход, а във finally „Терминалът е изключен.“ Извикай го с „25“ и с „двайсет“.
  • Средна оценка без сривове. Прочети 5 реда с оценки. Пропусни тези, които не са цели числа (изключение) или не са от 2 до 6 (if). Отпечатай средното с целочислено деление. Ако нито една оценка не е вярна, деленето на нула хвърля: хвани го и отпечатай „Няма верни оценки.“
  • Предвиди и провери. Напиши try с except и finally, в който всеки блок печата по ред. Предвиди изхода при успех, при хванато и при нехванато изключение и после го пусни.

Тест

Провери се

1. Изключението отговаря на два от трите except блока. Кой се изпълнява?
2. Хваща ли except ArithmeticError: изключение ZeroDivisionError?
3. Защо except Exception: трябва да е последен?
4. Кога се изпълнява finally?
5. Кога се изпълнява else след try/except?
6. Задължава ли те Python да хванеш ValueError около int()?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·