1/40
Wprowadzenie do wyjątków

Kiedy program "wybucha"

Nawet najlepiej napisany kod może napotkać problemy, na które programista nie ma bezpośredniego wpływu. Użytkownik może podać błędne dane, plik może nie istnieć, a połączenie z internetem może zostać przerwane.

W tradycyjnym programowaniu strukturalnym funkcje często zwracały kody błędów (np. -1 gdy coś poszło nie tak). Programowanie obiektowe wprowadza mechanizm wyjątków (exceptions).

  • Wyjątek: To obiekt, który "wyskakuje" (jest zgłaszany), gdy wystąpi błąd.
  • Obsługa: Pozwala programowi zareagować na błąd zamiast nagłego zakończenia pracy.
Symbol błędu i zatrzymania programu
2/40
Anatomia błędu w Pythonie

Co widzimy w konsoli?

Gdy błąd nie zostanie obsłużony, Python wyświetla tzw. Traceback. Jest to szczegółowy raport pokazujący, gdzie dokładnie wystąpił problem.

print(10 / 0)

# Wynik w konsoli:
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    print(10 / 0)
ZeroDivisionError: division by zero

Ostatnia linia to nazwa wyjątku (ZeroDivisionError) oraz szczegółowy komunikat. W Pythonie wyjątki są klasami.

3/40
Hierarchia wyjątków

Wszystko jest obiektem

Wyjątki w Pythonie tworzą strukturę drzewiastą (dziedziczenie). Wszystkie standardowe błędy wywodzą się z klasy BaseException.

  • BaseException
    • Exception (Główna baza dla błędów logicznych)
      • ArithmeticError
        • ZeroDivisionError
      • LookupError
        • IndexError

Zrozumienie tej hierarchii jest kluczowe, ponieważ obsłużenie klasy wyższej (rodzica) wyłapie również wszystkie błędy pochodne (dzieci).

4/40
Blok try-except

Próba i reakcja

Podstawowym mechanizmem obsługi błędów jest konstrukcja try-except. Pozwala ona "spróbować" wykonać kod, a jeśli wystąpi określony błąd – zareagować na niego.

try:
    liczba = int(input("Podaj liczbę: "))
    wynik = 100 / liczba
    print(f"Wynik: {wynik}")
except ValueError:
    print("Błąd: To nie jest liczba!")
except ZeroDivisionError:
    print("Błąd: Nie dziel przez zero!")

Jeśli kod w try wykona się bez błędu, sekcje except są pomijane.

5/40
Dostęp do obiektu wyjątku

Użycie aliasu 'as'

Często chcemy uzyskać dostęp do konkretnego obiektu wyjątku, aby przeczytać jego komunikat lub przekazać go dalej (np. do logów).

try:
    lista = [1, 2, 3]
    print(lista[5])
except IndexError as e:
    print(f"Wystąpił błąd: {e}")
    print(f"Typ błędu: {type(e)}")

Zmienna e przechowuje instancję klasy wyjątku, która została wygenerowana przez interpreter Pythona.

6/40
Obsługa wielu wyjątków naraz

Grupowanie błędów

Jeśli dla kilku różnych błędów chcemy wykonać tę samą akcję, możemy je zgrupować w krotce wewnątrz jednej klauzuli except.

try:
    # Ryzykowny kod...
    pass
except (IOError, EOFError) as błąd:
    print(f"Problem z wejściem/wyjściem: {błąd}")

Pozwala to zachować czytelność kodu i uniknąć powtarzania tych samych instrukcji obsługi błędu.

7/40
Klauzula else

Gdy wszystko pójdzie dobrze

Blok else wykonuje się tylko wtedy, gdy w bloku try nie wystąpił żaden wyjątek.

try:
    f = open("dane.txt", "r")
except FileNotFoundError:
    print("Nie znaleziono pliku.")
else:
    print("Plik otwarty pomyślnie!")
    zawartosc = f.read()
    f.close()

Dobrą praktyką jest trzymanie w bloku try tylko tego kodu, który faktycznie może wygenerować wyjątek, a resztę logiki przenosić do else.

8/40
Klauzula finally

Zawsze i wszędzie

Blok finally wykonuje się zawsze, niezależnie od tego, czy wystąpił wyjątek, czy nie (nawet jeśli użyto return).

try:
    f = open("dane.txt", "r")
    # operacje na pliku
finally:
    if 'f' in locals():
        f.close()
        print("Plik został zamknięty (posprzątano).")

Jest to idealne miejsce na "sprzątanie": zamykanie plików, zamykanie połączeń z bazą danych czy zwalnianie innych zasobów systemowych.

9/40
Zgłaszanie wyjątków: raise

Wymuszanie błędów

Programista może samodzielnie zgłosić wyjątek za pomocą słowa kluczowego raise. Pozwala to na przerwanie działania funkcji w sytuacjach, które są poprawne składniowo, ale błędne biznesowo.

def ustaw_wiek(wiek):
    if wiek < 0:
        raise ValueError("Wiek nie może być ujemny!")
    print(f"Ustawiono wiek: {wiek}")

try:
    ustaw_wiek(-5)
except ValueError as error:
    print(error)
10/40
Dlaczego własne wyjątki?

Precyzja ma znaczenie

Standardowe wyjątki Pythona jak ValueError czy TypeError są bardzo ogólne. Tworząc własne klasy wyjątków, sprawiamy, że kod staje się:

  • Bardziej czytelny: Nazwa wyjątku od razu mówi, co się stało (np. SaldoUjemneError).
  • Łatwiejszy w utrzymaniu: Możemy dokładnie odfiltrować błędy naszej domeny od błędów systemowych.
  • Bardziej funkcjonalny: Możemy do wyjątku dołączyć dodatkowe atrybuty (np. ID transakcji).
Zalety własnych wyjątków
11/40
Tworzenie własnej klasy wyjątku

Dziedziczenie po klasie Exception

Aby stworzyć własny wyjątek, wystarczy zdefiniować klasę, która dziedziczy po Exception (lub innej klasie wyjątku).

class MojBlad(Exception):
    """Moja własna klasa wyjątku."""
    pass

# Użycie:
raise MojBlad("Coś poszło nie tak w moim module")

Słowo pass wystarczy, jeśli nie potrzebujemy dodatkowej logiki – sam fakt istnienia nowej klasy (typu) jest już bardzo przydatny.

12/40
Wyjątki z dodatkowymi danymi

Przekazywanie kontekstu

Własna klasa wyjątku może mieć konstruktor __init__, który przyjmuje dodatkowe informacje o błędzie.

class BladWalidacji(Exception):
    def __init__(self, komunikat, pole, wartosc):
        super().__init__(komunikat)
        self.pole = pole
        self.wartosc = wartosc

try:
    raise BladWalidacji("Błędny e-mail", "email", "ala.com")
except BladWalidacji as e:
    print(f"Błąd w polu: {e.pole} (Wartość: {e.wartosc})")
13/40
Hierarchia własnych wyjątków

Dobre praktyki projektowe

W dużych projektach warto stworzyć jedną główną klasę błędu dla całej aplikacji/biblioteki, a następnie po niej dziedziczyć konkretne błędy.

class AppError(Exception): pass

class DatabaseError(AppError): pass
class ConfigError(AppError): pass

# Pozwala to użytkownikowi naszej biblioteki na:
except AppError:
    # Złapanie dowolnego błędu z naszej paczki
14/40
Przekazywanie wyjątku dalej

Raise bez argumentów

Czasem chcemy przechwycić wyjątek, zrobić coś (np. zapisać log) i puścić go dalej, aby wyższa warstwa aplikacji również o nim wiedziała.

try:
    wynik = 10 / 0
except ZeroDivisionError:
    print("Log: Próba dzielenia przez zero!")
    raise # Ponowne zgłoszenie tego samego wyjątku

Użycie samego raise wewnątrz bloku except powoduje ponowne zgłoszenie ostatnio aktywnego wyjątku wraz z jego oryginalnym śladem (traceback).

15/40
Łańcuchowanie wyjątków (Chaining)

Raise ... from ...

Gdy jeden wyjątek powoduje inny, możemy użyć klauzuli from, aby zachować informację o pierwotnej przyczynie (tzw. root cause – przyczynie źródłowej).

try:
    int("abc")
except ValueError as e:
    raise BladAplikacji("Niepoprawne dane") from e

Traceback pokaże wtedy komunikat: "The above exception was the direct cause of the following exception".

16/40
Ukrywanie przyczyny (from None)

Czysty komunikat dla użytkownika

Jeśli nie chcemy, aby użytkownik widział całą historię błędów systemowych (np. ze względów bezpieczeństwa lub estetyki), możemy ją ukryć.

try:
    kod_z_bledem()
except Exception:
    raise CustomError("Coś poszło nie tak") from None

Zapis from None przerywa łańcuch i pokazuje tylko nasz nowy wyjątek.

17/40
Najczęstszy błąd: Goły except

Dlaczego 'except:' jest groźny?

Zapis except: bez podania klasy łapie wszystkie wyjątki, w tym KeyboardInterrupt (Ctrl+C), co uniemożliwia zatrzymanie programu.

# BARDZO ZŁA PRAKTYKA:
try:
    zrob_cos()
except:
    pass  # Program "połyka" wszystkie błędy w ciszy
Zawsze staraj się łapać konkretne wyjątki. Jeśli naprawdę musisz złapać wszystko, użyj except Exception:, co oszczędzi wyjątki systemowe.
18/40
EAFP vs LBYL

Filozofie pisania kodu

W świecie programowania istnieją dwie główne szkoły podejścia do błędów:

  • LBYL (Look Before You Leap): Najpierw sprawdzasz, potem robisz (np. if os.path.exists()).
  • EAFP (Easier to Ask for Forgiveness than Permission): Robisz, a jak się nie uda, to obsługujesz błąd (użycie try-except).

Python preferuje model EAFP. Jest on często szybszy i bardziej odporny na wyścigi (np. plik zniknie między sprawdzeniem a otwarciem).

Porównanie EAFP i LBYL
19/40
Przykład praktyczny: Bankomat

Modelowanie błędów domeny

Wyobraźmy sobie system bankowy. Błędy takie jak brak środków to nie są błędy techniczne, ale sytuacje wyjątkowe w logice biznesowej.

class BankError(Exception): pass
class InsufficientFundsError(BankError): pass
class InvalidAmountError(BankError): pass

def wyplac(saldo, kwota):
    if kwota <= 0:
        raise InvalidAmountError("Kwota musi być dodatnia")
    if kwota > saldo:
        raise InsufficientFundsError(f"Brak środków. Brakuje {kwota-saldo}")
    return saldo - kwota
20/40
Obsługa błędów bankomatu

Interfejs użytkownika

Teraz możemy obsłużyć te błędy w miejscu, gdzie komunikujemy się z użytkownikiem, oddzielając logikę od prezentacji.

try:
    nowe_saldo = wyplac(100, 500)
except InsufficientFundsError as e:
    ostrzezenie(e)
except InvalidAmountError as e:
    blad_danych(e)
except BankError:
    nieznany_blad()

Taka struktura sprawia, że kod jest odporny na błędy i łatwy do zrozumienia.

21/40
Asercje: assert

Sposób na błędy programisty

Instrukcja assert służy do sprawdzania warunków, które zawsze powinny być prawdziwe. Jeśli warunek jest fałszywy, zgłaszany jest AssertionError.

def oblicz_znizke(cena, procent):
    assert 0 <= procent <= 100, "Procent musi być od 0 do 100"
    return cena * (1 - procent/100)
Asercje mogą zostać wyłączone przez optymalizator Pythona (flaga -O), więc nie używaj ich do walidacji danych od użytkownika. Służą tylko do debugowania i testowania założeń wewnątrz kodu.
22/40
Gdzie obsługiwać wyjątki?

Zasada odpowiedzialności

Nie każda funkcja powinna mieć blok try-except. Często lepiej pozwolić wyjątkowi "spaść" do warstwy wyżej, która wie, jak na niego zareagować.

  • Warstwa logiczna: Zgłasza wyjątki (raise).
  • Warstwa pośrednia: Może tłumaczyć wyjątki techniczne na biznesowe (chaining).
  • Warstwa UI / Kontroler: Przechwytuje wyjątki i wyświetla komunikat użytkownikowi.

Nazywa się to "propagacją wyjątków".

23/40
Zarządzanie zasobami: With

Context Managers

Wiele operacji wymagających try-finally (jak zamykanie plików) można zastąpić instrukcją with. Python zajmie się obsługą błędów i sprzątaniem za nas.

# Zamiast try-finally:
with open("test.txt") as f:
    dane = f.read()
# Plik zamknie się automatycznie, nawet po błędzie!

Obiekty używane w with implementują metody magiczne __enter__ i __exit__.

Zalety instrukcji with
24/40
Wyjątki a wydajność

Czy try-except spowalnia kod?

Krótka odpowiedź: nie. W Pythonie samo wejście w blok try jest praktycznie darmowe. Koszt pojawia się dopiero w momencie wystąpienia błędu.

Dlatego model EAFP ("spróbujmy") jest tak popularny – w optymistycznym scenariuszu, gdy błędy nie występują, kod działa z pełną prędkością bez zbędnych instrukcji if.

25/40
Logowanie błędów

Moduł logging

W profesjonalnych aplikacjach błędów nie wypisuje się za pomocą print. Używamy modułu logging, który pozwala zapisać błąd do pliku wraz z Tracebackiem.

import logging

try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Poważny błąd matematyczny!")

Metoda exception automatycznie dołącza pełną ścieżkę błędu do logów, co ułatwia debugowanie na serwerze.

26/40
Złożone struktury wyjątków

Wiele exceptów – kolejność ma znaczenie

Python sprawdza bloki except od góry do dołu. Pierwszy pasujący blok zostanie wykonany, reszta zignorowana.

try:
    raise ZeroDivisionError()
except ArithmeticError: # ZŁAPAŁO TUTAJ!
    print("Błąd arytmetyczny")
except ZeroDivisionError: # To się nigdy nie wykona
    print("Dzielenie przez zero")
Zawsze umieszczaj najbardziej szczegółowe wyjątki (dzieci) na górze, a te ogólne (rodziców) na dole.
27/40
Wyjątki w pętlach

Kontynuacja czy przerwanie?

Umiejscowienie bloku try w pętli decyduje o tym, czy błąd przerywa całe przetwarzanie, czy tylko dany krok.

# Błąd przeskakuje element:
for x in [1, 0, 2]:
    try:
        print(10 / x)
    except ZeroDivisionError:
        continue
28/40
Błędy składni vs Wyjątki

SyntaxError nie jest wyjątkiem runtime

Warto rozróżnić błędy, które zatrzymują program zanim w ogóle ruszy (błędy składni, SyntaxError), od wyjątków występujących w trakcie działania (błędy wykonania, Runtime Errors).

  • SyntaxError: Zapomniany dwukropek, błędy wcięć. Tych błędów nie da się obsłużyć za pomocą try-except.
  • Runtime Errors (błędy wykonania): Dzielenie przez zero, brak pliku. To dzieje się podczas pracy programu i tu stosujemy obsługę.
SyntaxError vs RuntimeError
29/40
Własny manager kontekstu

Zapanuj nad setupem i teardownem

Możemy stworzyć własną klasę, która automatycznie wykona coś na początku i na końcu bloku with.

class Timer:
    def __enter__(self):
        self.start = time.time()
        return self
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Czas: {time.time() - self.start}")

with Timer():
    operacja_trwajaca_dlugo()
30/40
Wyjątki w Programowaniu Obiektowym

Klasy jako kontenery wyjątków

Dobrą praktyką jest definiowanie wyjątków specyficznych dla danej klasy jako jej atrybutów lub w tym samym module.

class Parser:
    class ParseError(Exception): pass
    
    def analizuj(self, tekst):
        if not tekst:
            raise self.ParseError("Pusty tekst")

Ułatwia to organizację kodu w dużych systemach.

31/40
Podsumowanie podstaw

Co warto zapamiętać?

  • Wyjątki to obiekty reprezentujące błędy.
  • Używaj try-except do ochrony ryzykownych fragmentów.
  • finally służy do zwalniania zasobów.
  • raise pozwala ręcznie zgłosić błąd.
  • Własne wyjątki powinny dziedziczyć po Exception.
32/40
Przykładowy projekt: System Logowania

Praktyczne zastosowanie

Stwórzmy prosty system, który używa własnych wyjątków do obsługi błędów uwierzytelniania.

class AuthError(Exception): pass
class UserNotFoundError(AuthError): pass
class WrongPasswordError(AuthError): pass
class AccountLockedError(AuthError): pass
33/40
Logika systemu logowania

Implementacja metody logowania

def zaloguj(user, password):
    u = db.find_user(user)
    if not u:
        raise UserNotFoundError("Użytkownik nie istnieje")
    if u.is_locked:
        raise AccountLockedError("Konto zablokowane")
    if u.password != hash(password):
        raise WrongPasswordError("Błędne hasło")
    return "OK"
34/40
Wyjątki w testach jednostkowych

Testowanie błędów

Dobry test sprawdza nie tylko czy funkcja działa, ale też czy poprawnie "wybucha", gdy powinna. W pytest używamy do tego managera raises.

import pytest

def test_dzielenie_przez_zero():
    with pytest.raises(ZeroDivisionError):
        10 / 0
35/40
Wyjątki a typowanie (Type Hinting)

Dokumentowanie błędów

Niestety w Pythonie (inaczej niż w Javie czy C++) nie deklarujemy wyjątków w nagłówku funkcji. Dokumentujemy je w tzw. Docstringach.

def pobierz_dane(id: int) -> dict:
    """
    Pobiera dane z API.
    :raises ConnectionError: gdy brak internetu
    :raises ValueError: gdy ID jest błędne
    """
    pass
36/40
Ciekawostka: StopIteration

Wyjątki jako sterowanie przepływem

Nie wszystkie wyjątki to "błędy". StopIteration jest używany przez Pythona pod maską, aby zasygnalizować koniec pętli for.

To dowód na to, jak głęboko mechanizm wyjątków jest zakorzeniony w architekturze języka.

37/40
Złota zasada obsługi błędów

Nie ukrywaj problemów

Najgorsza obsługa błędu to taka, która go wyłapuje i nic z nim nie robi. Jeśli program nie może kontynuować pracy, powinien "zginąć" głośno, zamiast działać niepoprawnie w ciszy.

"Errors should never pass silently. Unless explicitly silenced." – Zen of Python
Zen of Python - błędy
38/40
Pytania kontrolne

Sprawdź swoją wiedzę

  1. Jaka klasa jest bazą dla wszystkich standardowych wyjątków?
  2. Czym różni się else od finally?
  3. Po co tworzymy własne klasy wyjątków?
  4. Co się stanie, jeśli użyjesz raise bez argumentów?
  5. Czy blok try bez except jest poprawny? (Tak, ale musi mieć finally).
39/40
Zadanie dla studenta

Napisz własny bezpieczny kalkulator

Napisz klasę Kalkulator, która obsługuje cztery podstawowe działania. Zdefiniuj własne wyjątki dla:

  • Dzielenia przez zero.
  • Próby wykonania operacji na typie innym niż liczba.
  • Wyniku, który przekracza zadany zakres (np. miliony).

Przetestuj obsługę tych wyjątków w głównym bloku programu.

40/40
Koniec części 9

Dziękuję za uwagę!

Znasz już mechanizmy rządzące błędami w Pythonie. Pamiętaj, że dobry programista to nie ten, który nie popełnia błędów, ale ten, który potrafi je przewidzieć i elegancko obsłużyć.

W kolejnej części zajmiemy się planowaniem projektu zaliczeniowego!

Zakończenie prezentacji