Как да се справя с изключенията в Python: Подробно визуално въведение

Добре дошли! В тази статия ще научите как да боравите с изключения в Python.

По-специално, ние ще обхванем:

  • Изключения
  • Целта на обработката на изключенията
  • Клауза за опит
  • Клаузата "изключение"
  • Клаузата else
  • Клаузата накрая
  • Как да се създадат изключения

Готов ли си? Нека да започнем! 😀

1️⃣ Въведение в изключенията

Ще започнем с изключения:

  • Кои са те
  • Защо са уместни?
  • Защо трябва да се справяте с тях?

Според документацията на Python:

Грешките, открити по време на изпълнение, се наричат изключения и не са безусловно фатални.

Изключения се създават, когато програмата срещне грешка по време на нейното изпълнение. Те нарушават нормалния поток на програмата и обикновено я приключват внезапно. За да избегнете това, можете да ги хванете и да се справите с тях по подходящ начин.

Вероятно сте ги виждали по време на вашите програмни проекти.

Ако някога сте опитвали да разделите на нула в Python, трябва да сте видели това съобщение за грешка:

>>> a = 5/0 Traceback (most recent call last): File "", line 1, in  a = 5/0 ZeroDivisionError: division by zero

Ако сте се опитали да индексирате низ с невалиден индекс, определено сте получили това съобщение за грешка:

>>> a = "Hello, World" >>> a[456] Traceback (most recent call last): File "", line 1, in  a[456] IndexError: string index out of range

Това са примери за изключения.

🔹 Чести изключения

Има много различни видове изключения и всички те се повдигат в определени ситуации. Някои от изключенията, които най-вероятно ще видите, докато работите по проектите си, са:

  • IndexError - повдига се, когато се опитвате да индексирате списък, кортеж или низ извън разрешените граници. Например:
>>> num = [1, 2, 6, 5] >>> num[56546546] Traceback (most recent call last): File "", line 1, in  num[56546546] IndexError: list index out of range
  • KeyError - повдига се при опит за достъп до стойността на ключ, който не съществува в речник. Например:
>>> students = {"Nora": 15, "Gino": 30} >>> students["Lisa"] Traceback (most recent call last): File "", line 1, in  students["Lisa"] KeyError: 'Lisa'
  • NameError - извежда се , когато име, на което се позовавате в кода, не съществува. Например:
>>> a = b Traceback (most recent call last): File "", line 1, in  a = b NameError: name 'b' is not defined
  • TypeError - извежда се , когато операция или функция е приложена към обект от неподходящ тип. Например:
>>> (5, 6, 7) * (1, 2, 3) Traceback (most recent call last): File "", line 1, in  (5, 6, 7) * (1, 2, 3) TypeError: can't multiply sequence by non-int of type 'tuple'
  • ZeroDivisionError - повдига се, когато се опитате да разделите на нула.
>>> a = 5/0 Traceback (most recent call last): File "", line 1, in  a = 5/0 ZeroDivisionError: division by zero

💡 Съвети: За да научите повече за други видове вградени изключения, моля, вижте тази статия в Документацията Python.

🔸 Анатомия на изключение

Сигурен съм, че трябва да сте забелязали общ модел в тези съобщения за грешка. Нека разделим общата им структура парче по парче:

Първо, намираме този ред (виж по-долу). А traceback е в основата на списък с подробно описание на извиквания на функции, които са били направени преди изключение е бил повдигнат.

Проследяването ви помага по време на процеса на отстраняване на грешки, защото можете да анализирате последователността от извиквания на функции, които са довели до изключението:

Traceback (most recent call last):

След това виждаме този ред (вижте по-долу) с пътя към файла и реда, който е повдигнал изключението. В този случай пътят беше обвивката на Python, тъй като примерът беше изпълнен директно в IDLE.

File "", line 1, in  a - 5/0

💡 Съвет: Ако редът, който е повдигнал изключението, принадлежи на функция, се заменя с името на функцията.

И накрая, виждаме описателно съобщение, което подробно описва вида на изключението и предоставя допълнителна информация, която да ни помогне да отстраним грешките в кода:

NameError: name 'a' is not defined

2️⃣ Обработка на изключения: Цел и контекст

Може да попитате: защо бих искал да се справя с изключенията? Защо това е полезно за мен? Чрез обработка на изключения можете да осигурите алтернативен поток на изпълнение, за да избегнете неочаквано срив на вашата програма.

🔹 Пример: Потребителско въвеждане

Представете си какво би се случило, ако потребител, който работи с вашата програма, въведе невалиден вход. Това би довело до изключение, тъй като по време на процеса е извършена невалидна операция.

Ако вашата програма не се справи с това правилно, тя ще се срине внезапно и потребителят ще има много разочароващо преживяване с вашия продукт.

Но ако се справите с изключението, ще можете да предоставите алтернатива за подобряване на практическата работа на потребителя.

Perhaps you could display a descriptive message asking the user to enter a valid input, or you could provide a default value for the input. Depending on the context, you can choose what to do when this happens, and this is the magic of error handling. It can save the day when unexpected things happen. ⭐️

🔸 What Happens Behind the Scenes?

Basically, when we handle an exception, we are telling the program what to do if the exception is raised. In that case, the "alternative" flow of execution will come to the rescue. If no exceptions are raised, the code will run as expected.

3️⃣ Time to Code: The try ... except Statement

Now that you know what exceptions are and why you should we handle them, we will start diving into the built-in tools that the Python languages offers for this purpose.

First, we have the most basic statement: try ... except.

Let's illustrate this with a simple example. We have this small program that asks the user to enter the name of a student to display his/her age:

students = {"Nora": 15, "Gino": 30} def print_student_age(): name = input("Please enter the name of the student: ") print(students[name]) print_student_age()

Notice how we are not validating user input at the moment, so the user might enter invalid values (names that are not in the dictionary) and the consequences would be catastrophic because the program would crash if a KeyError is raised:

# User Input Please enter the name of the student: "Daniel" # Error Message Traceback (most recent call last): File "", line 15, in  print_student_age() File "", line 13, in print_student_age print(students[name]) KeyError: '"Daniel"'

🔹 Syntax

We can handle this nicely using try ... except. This is the basic syntax:

In our example, we would add the try ... except statement within the function. Let's break this down piece by piece:

students = {"Nora": 15, "Gino": 30} def print_student_age(): while True: try: name = input("Please enter the name of the student: ") print(students[name]) break except: print("This name is not registered") print_student_age()

If we "zoom in", we see the try ... except statement:

try: name = input("Please enter the name of the student: ") print(students[name]) break except: print("This name is not registered")
  • When the function is called, the try clause will run. If no exceptions are raised, the program will run as expected.
  • But if an exception is raised in the try clause, the flow of execution will immediately jump to the except clause to handle the exception.

💡 Note: This code is contained within a while loop to continue asking for user input if the value is invalid. This is an example:

Please enter the name of the student: "Lulu" This name is not registered Please enter the name of the student: 

This is great, right? Now we can continue asking for user input if the value is invalid.

At the moment, we are handling all possible exceptions with the same except clause. But what if we only want to handle a specific type of exception? Let's see how we could do this.

🔸 Catching Specific Exceptions

Since not all types of exceptions are handled in the same way, we can specify which exceptions we would like to handle with this syntax:

This is an example. We are handling the ZeroDivisionError exception in case the user enters zero as the denominator:

def divide_integers(): while True: try: a = int(input("Please enter the numerator: ")) b = int(input("Please enter the denominator: ")) print(a / b) except ZeroDivisionError: print("Please enter a valid denominator.") divide_integers()

Това би бил резултатът:

# First iteration Please enter the numerator: 5 Please enter the denominator: 0 Please enter a valid denominator. # Second iteration Please enter the numerator: 5 Please enter the denominator: 2 2.5

Ние се справяме с това правилно. Но ... ако се повдигне друг вид изключение, програмата няма да се справи с него елегантно.

Тук имаме пример за ValueError, защото една от стойностите е float, а не int:

Please enter the numerator: 5 Please enter the denominator: 0.5 Traceback (most recent call last): File "", line 53, in  divide_integers() File "", line 47, in divide_integers b = int(input("Please enter the denominator: ")) ValueError: invalid literal for int() with base 10: '0.5'

Можем да персонализираме как се справяме с различни видове изключения.

Множество изключителни клаузи

За да направим това, трябва да добавим множество exceptклаузи, за да обработваме различните видове изключения по различен начин.

Според документацията на Python:

Операторът try може да има повече от една клауза , за да посочи манипулатори за различни изключения. Ще бъде изпълнен най-много един манипулатор .

В този пример имаме две с изключение на клаузи. Един от тях обработва ZeroDivisionError, а другият обработва ValueError, двата типа изключения, които могат да бъдат повдигнати в този блок за опити.

def divide_integers(): while True: try: a = int(input("Please enter the numerator: ")) b = int(input("Please enter the denominator: ")) print(a / b) except ZeroDivisionError: print("Please enter a valid denominator.") except ValueError: print("Both values have to be integers.") divide_integers() 

💡 Съвет: Трябва да определите кои видове изключения могат да бъдат повдигнати в блока за опит, за да се справите с тях по подходящ начин.

🔸 Множество изключения, една изключителна клауза

Можете също така да изберете да обработвате различни видове изключения с една и съща клауза.

Според документацията на Python:

Клауза изключение може да посочи множество изключения като скоба в скоби.

Това е пример, където улавяме две изключения (ZeroDivisionError и ValueError) със същата exceptклауза:

def divide_integers(): while True: try: a = int(input("Please enter the numerator: ")) b = int(input("Please enter the denominator: ")) print(a / b) except (ZeroDivisionError, ValueError): print("Please enter valid integers.") divide_integers()

Резултатът ще бъде еднакъв за двата типа изключения, тъй като те се обработват от една и съща клауза с изключение:

Please enter the numerator: 5 Please enter the denominator: 0 Please enter valid integers.
Please enter the numerator: 0.5 Please enter valid integers. Please enter the numerator: 

🔹 Обработка на изключения, създадени от функции, извикани в клаузата за опит

Интересен аспект на обработката на изключения е, че ако се повдигне изключение във функция, която преди това е била извикана в клаузата try на друга функция и самата функция не се справя с нея, повикващият ще се справи с нея, ако има подходяща клауза с изключение.

Според документацията на Python:

Манипулаторите на изключения не се справят само с изключенията, ако възникнат незабавно в клаузата try, но също така и ако се появят във функции, които са извикани (дори косвено) в клаузата try.

Нека да видим пример, който да илюстрира това:

def f(i): try: g(i) except IndexError: print("Please enter a valid index") def g(i): a = "Hello" return a[i] f(50)

Имаме fфункцията и gфункцията. fобаждания gв клаузата try. С аргумента 50 gще изведе IndexError, защото индексът 50 не е валиден за низа a.

But g itself doesn't handle the exception. Notice how there is no try ... except statement in the g function. Since it doesn't handle the exception, it "sends" it to f to see if it can handle it, as you can see in the diagram below:

Since f does know how to handle an IndexError, the situation is handled gracefully and this is the output:

Please enter a valid index

💡 Note: If f had not handled the exception, the program would have ended abruptly with the default error message for an IndexError.

🔸 Accessing Specific Details of Exceptions

Exceptions are objects in Python, so you can assign the exception that was raised to a variable. This way, you can print the default description of the exception and access its arguments.

Според документацията на Python:

Клаузата изключение може да указва променлива след името на изключението . Променливата е обвързана с екземпляр на изключение с аргументите, съхранявани в instance.args.

Тук имаме пример (вижте по-долу), когато присвояваме екземпляра на ZeroDivisionErrorпроменливата e. След това можем да използваме тази променлива в изключителната клауза за достъп до типа на изключението, неговото съобщение и аргументи.

def divide_integers(): while True: try: a = int(input("Please enter the numerator: ")) b = int(input("Please enter the denominator: ")) print(a / b) # Here we assign the exception to the variable e except ZeroDivisionError as e: print(type(e)) print(e) print(e.args) divide_integers()

Съответният изход ще бъде:

Please enter the numerator: 5 Please enter the denominator: 0 # Type  # Message division by zero # Args ('division by zero',)

💡 Съвет: Ако сте запознати със специални методи, съгласно Документацията на Python: „за удобство екземплярът на изключението дефинира, __str__()така че аргументите да могат да бъдат отпечатани директно, без да се налага да се препраща .args.

4️⃣ Сега нека добавим: Клаузата „else“

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

Според документацията на Python:

Операторът try... exceptима незадължителна клауза else , която, когато е налице, трябва да следва всички клаузи с изключение. Полезно е за код, който трябва да бъде изпълнен, ако клаузата try не създава изключение.

Ето пример за използването на elseклаузата:

def divide_integers(): while True: try: a = int(input("Please enter the numerator: ")) b = int(input("Please enter the denominator: ")) result = a / b except (ZeroDivisionError, ValueError): print("Please enter valid integers. The denominator can't be zero") else: print(result) divide_integers()

Ако не се повдигне изключение, резултатът се отпечатва:

Please enter the numerator: 5 Please enter the denominator: 5 1.0

Но ако се повдигне изключение, резултатът не се отпечатва:

Please enter the numerator: 5 Please enter the denominator: 0 Please enter valid integers. The denominator can't be zero

💡 Съвет: Според документацията на Python:

Използването на elseклаузата е по-добре от добавянето на допълнителен код към tryклаузата, тъй като се избягва случайно улавяне на изключение, което не е повдигнато от кода, защитен от try... exceptизраза.

5️⃣ Клаузата „окончателно“

Клаузата final е последната клауза в тази последователност. Той не е задължителен , но ако го включите, той трябва да бъде последната клауза в последователността. В finallyклаузата е винаги изпълнена, дори и ако по изключение е бил повдигнат в клаузата за опита.  

Според документацията на Python:

Ако има finallyклауза, finallyклаузата ще се изпълни като последната задача преди tryоператора да завърши. В finallyклаузата работи независимо дали tryизявлението произвежда изключение.

Клаузата final обикновено се използва за извършване на действия за "почистване", които винаги трябва да бъдат завършени. Например, ако работим с файл в клаузата try, винаги ще трябва да затворим файла, дори ако е било повдигнато изключение, когато сме работили с данните.

Ето пример за клаузата final:

def divide_integers(): while True: try: a = int(input("Please enter the numerator: ")) b = int(input("Please enter the denominator: ")) result = a / b except (ZeroDivisionError, ValueError): print("Please enter valid integers. The denominator can't be zero") else: print(result) finally: print("Inside the finally clause") divide_integers()

Това е резултатът, когато не са били повдигнати изключения:

Please enter the numerator: 5 Please enter the denominator: 5 1.0 Inside the finally clause

Това е изходът, когато е повдигнато изключение:

Please enter the numerator: 5 Please enter the denominator: 0 Please enter valid integers. The denominator can't be zero Inside the finally clause

Забележете как finallyклаузата винаги работи.

❗️Important: remember that the else clause and the finally clause are optional, but if you decide to include both, the finally clause has to be the last clause in the sequence.

6️⃣ Raising Exceptions

Now that you know how to handle exceptions in Python, I would like to share with you this helpful tip: you can also choose when to raise exceptions in your code.

This can be helpful for certain scenarios. Let's see how you can do this:

This line will raise a ValueError with a custom message.

Here we have an example (see below) of a function that prints the value of the items of a list or tuple, or the characters in a string. But you decided that you want the list, tuple, or string to be of length 5. You start the function with an if statement that checks if the length of the argument data is 5. If it isn't, a ValueError exception is raised:

def print_five_items(data): if len(data) != 5: raise ValueError("The argument must have five elements") for item in data: print(item) print_five_items([5, 2])

The output would be:

Traceback (most recent call last): File "", line 122, in  print_five_items([5, 2]) File "", line 117, in print_five_items raise ValueError("The argument must have five elements") ValueError: The argument must have five elements

Notice how the last line displays the descriptive message:

ValueError: The argument must have five elements

You can then choose how to handle the exception with a try ... except statement. You could add an else clause and/or a finally clause. You can customize it to fit your needs.

🔹 Helpful Resources

  • Exceptions
  • Handling Exceptions
  • Defining Clean-up Actions

I hope you enjoyed reading my article and found it helpful. Now you have the necessary tools to handle exceptions in Python and you can use them to your advantage when you write Python code. ? Check out my online courses. You can follow me on Twitter.

⭐️ You may enjoy my other freeCodeCamp /news articles:

  • The @property Decorator in Python: Its Use Cases, Advantages, and Syntax
  • Data Structures 101: Graphs — A Visual Introduction for Beginners
  • Data Structures 101: Arrays — A Visual Introduction for Beginners