Witajcie! Dziś zaczynamy serię o wyjątkach w Pythonie. To temat, który początkowo może wydawać się skomplikowany, ale gdy już go opanujecie, stanie się waszym najlepszym przyjacielem w walce z błędami.
Wyjątki to nie tylko sposób na radzenie sobie z błędami - to fundamentalny mechanizm, który czyni Python tak przyjaznym dla programistów. W pierwszej części wyjaśnię wszystko od podstaw, żebyście zrozumieli nie tylko "jak", ale przede wszystkim "dlaczego".
Czym właściwie są wyjątki?
Zanim przejdziemy do kodowania, wyjaśnijmy sobie najpierw, czym w ogóle są wyjątki.
Wyjątek to specjalny obiekt w Pythonie, który sygnalizuje, że coś poszło nie tak podczas wykonywania programu. To jak alarm przeciwpożarowy - gdy go słyszycie, wiecie, że trzeba podjąć jakieś działania.
Zobaczmy najprostszy przykład:
print("Zaczynam program")
wynik = 10 / 0 # Tu nastąpi błąd!
print("Ten napis się nie wyświetli")Kiedy uruchomicie ten kod, Python rzuci wyjątek ZeroDivisionError. Program zatrzyma się i wyświetli błąd. To nie znaczy, że Python jest słaby - to znaczy, że chroni was przed wykonaniem niemożliwej operacji.
Dlaczego wyjątki są lepsze niż kody błędów?
Żeby zrozumieć, dlaczego wyjątki to genialne rozwiązanie, porównajmy je z innymi sposobami obsługi błędów.
Jak to robiono kiedyś (w języku C)
W starszych językach programowania używano kodów błędów:
// Przykład w języku C
int wynik;
int kod_bledu = podziel(10, 0, &wynik);
if (kod_bledu != 0) {
printf("Błąd: nie można dzielić przez zero");
return;
}
printf("Wynik: %d", wynik);Problemy z tym podejściem:
- Łatwo zapomnieć sprawdzić kod błędu
- Kod staje się bardzo skomplikowany
- Trzeba pamiętać o sprawdzaniu błędów po każdym wywołaniu funkcji
Jak to działa w Pythonie
Python rozwiązuje te problemy elegancko:
try:
wynik = 10 / 0
print(f"Wynik: {wynik}")
except ZeroDivisionError:
print("Błąd: nie można dzielić przez zero")Zalety tego podejścia:
- Niemożliwość zignorowania błędu - jeśli nie obsłużycie wyjątku, program się zatrzyma
- Czystszy kod - główna logika jest oddzielona od obsługi błędów
- Automatyczna propagacja - błędy "bąbelkują" w górę, o czym za moment
Hierarchia wyjątków - dlaczego ma znaczenie
Python organizuje wyjątki w hierarchię, która ma praktyczny sens. Oto uproszczona wersja:
BaseException
├── SystemExit # Wyjście z programu (exit())
├── KeyboardInterrupt # Ctrl+C
└── Exception # Tu są wszystkie "normalne" wyjątki
├── ValueError # Błędna wartość
├── TypeError # Błędny typ
├── IndexError # Indeks poza zakresem
├── KeyError # Brakujący klucz w słowniku
├── FileNotFoundError # Nie znaleziono pliku
└── ... # I wiele innychDlaczego to ważne? Bo pozwala wam łapać wyjątki na różnych poziomach ogólności.
Przykład praktyczny - obsługa różnych błędów
def pobierz_dane_uzytkownika():
try:
wiek = int(input("Podaj swój wiek: "))
if wiek < 0:
raise ValueError("Wiek nie może być ujemny")
return wiek
except ValueError as e:
print(f"Nieprawidłowa wartość: {e}")
return None
except KeyboardInterrupt:
print("\nProgram przerwany przez użytkownika")
return None
# Testowanie
wiek = pobierz_dane_uzytkownika()
if wiek is not None:
print(f"Twój wiek to: {wiek}")Zwróćcie uwagę na as e - to pozwala nam "złapać" obiekt wyjątku i przeczytać jego wiadomość. Ponadto, w różnych liniach łapiemy różne wyjątki.
Mechanizm "bąbelkowania" - jak wyjątki podróżują
To jedna z najważniejszych rzeczy do zrozumienia. Wyjątki w Pythonie "bąbelkują" w górę stosu wywołań, jak bąbelki powietrza w wodzie.
def poziom_3():
print("Poziom 3: Wywołuję dzielenie")
return 10 / 0 # Tu powstaje wyjątek
def poziom_2():
print("Poziom 2: Wywołuję poziom 3")
return poziom_3() # Wyjątek przechodzi przez tę funkcję
def poziom_1():
print("Poziom 1: Wywołuję poziom 2")
try:
wynik = poziom_2()
return wynik
except ZeroDivisionError:
print("Poziom 1: Złapałem błąd dzielenia!")
return None
# Uruchomienie
poziom_1()
Poziom 1: Wywołuję poziom 2
Poziom 2: Wywołuję poziom 3
Poziom 3: Wywołuję dzielenie
Poziom 1: Złapałem błąd dzielenia!Co się dzieje:
- Wyjątek powstaje w
poziom_3() - Python nie znajdując obsługi w
poziom_3(), przekazuje wyjątek dopoziom_2() poziom_2()też nie ma obsługi, więc wyjątek idzie dalejpoziom_1()ma bloktry-except, więc wyjątek zostaje złapany
To jest potęga wyjątków - możecie obsłużyć błąd na dowolnym poziomie w waszym programie!
Podstawowa składnia - try, except, else, finally
Pełna składnia obsługi wyjątków wygląda tak:
try:
# Kod, który może powodować błędy
pass
except TypWyjatku1:
# Co robić gdy wystąpi TypWyjatku1
pass
except TypWyjatku2:
# Co robić gdy wystąpi TypWyjatku2
pass
else:
# Co robić gdy NIE wystąpił żaden wyjątek
pass
finally:
# Co robić ZAWSZE (bez względu na wyjątki)
passKluczowe informacje:
elsewykonuje się tylko gdy nie było wyjątków. Pozwala świetnie rozdzielić "ryzykowny" kod od bezpiecznego.finallywykonuje się zawsze, nawet gdy mamyreturnw blokutry- To pozwala na pewne "sprzątanie" zasobów
Praktyczny przykład z plikami
def przeczytaj_plik(nazwa_pliku):
plik = None
print(f"Próbuję otworzyć plik: {nazwa_pliku}")
try:
plik = open(nazwa_pliku, 'r', encoding='utf-8') # operacja ryzykowna
zawartość = plik.read() # operacja ryzykowna
except FileNotFoundError:
print(f"Plik {nazwa_pliku} nie istnieje!")
return None
except PermissionError:
print(f"Brak uprawnień do odczytu pliku {nazwa_pliku}")
return None
except UnicodeDecodeError:
print(f"Nie mogę zdekodować pliku {nazwa_pliku} jako UTF-8")
return None
else:
# Ten blok wykonuje się tylko gdy nie było błędów
print("Plik odczytany pomyślnie!")
return zawartość
finally:
# Ten blok wykonuje się ZAWSZE
if plik is not None:
plik.close()
print("Plik zamknięty")
# Testowanie
zawartość = przeczytaj_plik("test.txt")
if zawartość:
print(f"Zawartość pliku: {zawartość[:100]}...") # Pierwsze 100 znakówZwróćcie uwagę na else - wykonuje się tylko gdy nie było wyjątku. Można tutaj umieścić kod przetwarzający dane z pliku, nim go zwrócimy. Umieszczenie tej logiki w try tylko by zmniejszyło czytelność.
Rzucanie własnych wyjątków
Czasami chcecie sami rzucić wyjątek. Do tego służy słowo raise:
def sprawdz_wiek(wiek):
if wiek < 0:
raise ValueError("Wiek nie może być ujemny")
if wiek > 150:
raise ValueError("Wiek wydaje się nierealistyczny")
return True
def zarejestruj_uzytkownika(imie, wiek):
try:
sprawdz_wiek(wiek)
print(f"Rejestruję użytkownika: {imie}, wiek: {wiek}")
return True
except ValueError as e:
print(f"Błąd rejestracji: {e}")
return False
# Testowanie
zarejestruj_uzytkownika("Anna", 25) # OK
zarejestruj_uzytkownika("Jan", -5) # Błąd
zarejestruj_uzytkownika("Maria", 200) # BłądNajczęstsze wyjątki i kiedy występują
Poznajmy najważniejsze wyjątki, z którymi będziecie się spotykać:
ValueError - błędna wartość
try:
liczba = int("to nie jest liczba")
except ValueError:
print("Nie mogę przekonwertować tekstu na liczbę")
try:
import math
math.sqrt(-1) # Pierwiastek z liczby ujemnej
except ValueError:
print("Nie mogę policzyć pierwiastka z liczby ujemnej")TypeError - błędny typ
try:
wynik = "tekst" + 5 # Nie można dodać tekstu do liczby
except TypeError:
print("Nie mogę dodać tekstu do liczby")
try:
lista = [1, 2, 3]
lista[1.5] # Indeks musi być liczbą całkowitą
except TypeError:
print("Indeks musi być liczbą całkowitą")IndexError - indeks poza zakresem
lista = [1, 2, 3]
try:
element = lista[10] # Lista ma tylko 3 elementy (indeksy 0, 1, 2)
except IndexError:
print("Indeks poza zakresem listy")KeyError - brakujący klucz w słowniku
slownik = {"imie": "Anna", "wiek": 25}
try:
miasto = slownik["miasto"] # Klucz "miasto" nie istnieje
except KeyError:
print("Brakuje klucza 'miasto' w słowniku")FileNotFoundError - nie znaleziono pliku
try:
with open("nieistniejacy_plik.txt", "r") as plik:
zawartość = plik.read()
except FileNotFoundError:
print("Plik nie istnieje")Dobre praktyki dla początkujących
1. Zawsze łapcie konkretne wyjątki
# ZŁE - zbyt ogólne
try:
liczba = int(input("Podaj liczbę: "))
except Exception: # Łapie wszystko!
print("Coś poszło nie tak")
# DOBRE - konkretne
try:
liczba = int(input("Podaj liczbę: "))
except ValueError:
print("To nie jest poprawna liczba")
except KeyboardInterrupt:
print("Program przerwany")Łapanie exception (ogólnego wyjątku) to bardzo zła praktyka. Jest to zbyt ogólne, nie pozwala na obsługę specyficznych błędów. Oczywiście, można pod except narobić mase warunków if i obsługiwać różne opcje, ale po co?
2. Nie ignorujcie wyjątków
# ZŁE - ignorowanie błędów
try:
ryzykowna_operacja()
except:
pass # Błąd znika bez śladu!
# DOBRE - zawsze coś robimy z błędem
try:
ryzykowna_operacja()
except ValueError as e:
print(f"Wystąpił błąd: {e}")
# I podjąć jakieś działanie naprawczeZnikanie beź śladu to proszenie się o kłopoty. Nie wiemy co padło w programie, i to może spowodować potężne problemy. A poza tym łamie Zen of Python: Errors should never pass silently.
3. Używajcie opisowych komunikatów
def podziel_liczby(a, b):
if b == 0:
raise ValueError(f"Nie można dzielić {a} przez zero")
return a / b
try:
wynik = podziel_liczby(10, 0)
except ValueError as e:
print(f"Błąd obliczeń: {e}")Praktyczny przykład - kalkulator odporny na błędy
Zobaczmy, jak zastosować wiedzę o wyjątkach w praktyce:
def kalkulator():
"""Prosty kalkulator z obsługą błędów"""
while True:
try:
print("\n=== KALKULATOR ===")
print("Dostępne operacje: +, -, *, /")
print("Wpisz 'quit' aby wyjść")
operacja = input("Podaj operację (np. 5 + 3): ").strip()
if operacja.lower() == 'quit':
print("Do widzenia!")
break
# Sprawdzamy czy operacja zawiera jeden z operatorów
if '+' in operacja:
a, b = operacja.split('+')
wynik = float(a.strip()) + float(b.strip())
elif '-' in operacja:
a, b = operacja.split('-')
wynik = float(a.strip()) - float(b.strip())
elif '*' in operacja:
a, b = operacja.split('*')
wynik = float(a.strip()) * float(b.strip())
elif '/' in operacja:
a, b = operacja.split('/')
a_val = float(a.strip())
b_val = float(b.strip())
if b_val == 0:
raise ZeroDivisionError("Nie można dzielić przez zero")
wynik = a_val / b_val
else:
raise ValueError("Nieznana operacja. Użyj +, -, * lub /")
print(f"Wynik: {wynik}")
except ValueError as e:
if "could not convert" in str(e):
print("Błąd: Podaj poprawne liczby")
else:
print(f"Błąd: {e}")
except ZeroDivisionError as e:
print(f"Błąd: {e}")
except KeyboardInterrupt:
print("\nProgram przerwany. Do widzenia!")
break
except Exception as e:
# tak, łapiemy wszystko i wyświetlamy. To jest ok, program nie padnie. Tego oczekujemy.
print(f"Nieoczekiwany błąd: {e}")
# Uruchomienie kalkulatora
kalkulator()Ten kalkulator radzi sobie z różnymi problemami:
- Nieprawidłowe liczby
- Dzielenie przez zero
- Nieznane operacje
- Przerwanie programu (Ctrl+C)
Kiedy używać wyjątków?
Używajcie wyjątków gdy:
Operacja może się nie powieść z przyczyn zewnętrznych
try: with open("config.txt") as f: config = f.read() except FileNotFoundError: print("Brak pliku konfiguracyjnego, używam domyślnych ustawień") config = "domyślna konfiguracja"Chcecie walidować dane użytkownika
def sprawdz_email(email): if '@' not in email: raise ValueError("Email musi zawierać znak @") if '.' not in email.split('@')[1]: raise ValueError("Email musi mieć poprawną domenę") return emailImplementujecie API, które może zwrócić błąd
def pobierz_dane_z_api(url): try: odpowiedz = requests.get(url, timeout=5) odpowiedz.raise_for_status() # Rzuci wyjątek przy błędzie HTTP return odpowiedz.json() except requests.exceptions.Timeout: raise ConnectionError("Serwer nie odpowiada w czasie") except requests.exceptions.HTTPError as e: raise ValueError(f"Błąd serwera: {e}")
NIE używajcie wyjątków gdy:
Sprawdzacie normalny flow programu
# ZŁE - używanie wyjątków do kontroli przepływu try: if user.is_admin: perform_admin_action() else: raise Exception("Brak uprawnień") except Exception: perform_user_action() # DOBRE - zwykły if if user.is_admin: perform_admin_action() else: perform_user_action()
Podsumowanie i co dalej
W tej części nauczyliście się podstaw wyjątków w Pythonie:
- Wyjątki to obiekty, które sygnalizują problemy w programie
- Mechanizm bąbelkowania pozwala obsługiwać błędy na odpowiednim poziomie
- Hierarchia wyjątków umożliwia łapanie błędów na różnych poziomach ogólności
- Składnia try-except-else-finally daje pełną kontrolę nad obsługą błędów
- Dobre praktyki to konkretne wyjątki i opisowe komunikaty
Najważniejsze do zapamiętania:
- Wyjątki to normalna część programowania, nie oznaczają błędu programisty
- Zawsze obsługujcie błędy, których się spodziewacie
- Używajcie konkretnych typów wyjątków zamiast ogólnych
- Pamiętajcie o bloku
finallydo sprzątania zasobów
W części drugiej omówimy:
- Tworzenie własnych klas wyjątków
- Context managery i automatyczne zarządzanie zasobami
- Wydajność wyjątków i kiedy ich unikać
- Debugowanie i śledzenie błędów
- Integracja z systemami logowania
W części trzeciej zajmiemy się:
- Wyjątkami w programowaniu asynchronicznym
- Obsługą błędów w frameworkach (Django, Flask)
- Testowaniem kodu z wyjątkami
- Zaawansowanymi wzorcami i optymalizacją
Mam nadzieję, że ta pierwsza część dała wam solidne podstawy! Teraz możecie śmiało eksperymentować z wyjątkami w swoich projektach. Pamiętajcie - każdy błąd to okazja do nauki i poprawy kodu.
Do zobaczenia w kolejnej części!
Spodobał Ci się post? Sprawdź pozostałe części serii lub podziel się nim!
Masz uwagi do posta, chcesz porozmawiać, szukasz pomocy z Pythonem i Django? Napisz do mnie!
Zapraszam do zadawania pytań przez formularz kontaktowy. Pamiętaj, że jeśli potrzebujesz wsparcia, możesz napisać do mnie - pomogę.
Jeśli jesteście zainteresowani pogłębieniem wiedzy o Django i Pythonie, zapraszam do dołączenia do społeczności PyMasters, gdzie regularnie omawiamy najnowsze technologie i najlepsze praktyki. A dla początkujących programistów przygotowałem darmowego ebooka "WARSZTAT JUNIORA: Przewodnik po kluczowych kompetencjach i narzędziach dla początkującego programisty Pythona".