← Strona główna

17 — Debugowanie i błędy

Typowe wyjątki, czytanie traceback, techniki znajdowania i naprawy błędów.

Egzamin i wsparcie

1. Jak czytać komunikat błędu (traceback)?

Gdy Python napotka błąd — wypisuje traceback (stos wywołań). Czytaj go od dołu — ostatnia linia to typ błędu i jego opis, linie wyżej to ścieżka do miejsca błędu.

  • Ostatnia linia: NazwaWyjatku: opis — co poszło nie tak
  • Linia File "...", line X, in ... — gdzie dokładnie
  • Linia kodu — co Python próbował wykonać
  • Czytaj od dołu do góry — dół = miejsce błędu, góra = skąd wywołano
Strategia debugowania 1. Przeczytaj ostatnią linię traceback
2. Znajdź numer linii w swoim kodzie
3. Sprawdź typy zmiennych w tej linii
4. Dodaj print() przed błędem żeby zobaczyć wartości
5. Napraw i przetestuj ponownie
Przykład traceback — analiza
# Kod z błędem:
def oblicz_srednia(oceny):
    return sum(oceny) / len(oceny)

klasa = {"Kasia": [5,4,5], "Marek": []}
for imie, oceny in klasa.items():
    print(oblicz_srednia(oceny))

# Traceback (most recent call last):
#   File "program.py", line 6, in <module>
#     print(oblicz_srednia(oceny))
#   File "program.py", line 2, in oblicz_srednia
#     return sum(oceny) / len(oceny)
# ZeroDivisionError: division by zero
#
# Czytamy od dołu:
# → ZeroDivisionError: dzielenie przez zero
# → w funkcji oblicz_srednia, linia 2
# → wywołana z linii 6 w głównym kodzie
# → Marek ma pustą listę ocen!
#
# Naprawa:
def oblicz_srednia(oceny):
    if not oceny:
        return 0
    return sum(oceny) / len(oceny)

2. Katalog typowych błędów — przyczyny i naprawy

WyjątekTypowa przyczynaNaprawa
SyntaxError błąd składni — brak dwukropka, nawiasu, błędne wcięcie sprawdź linię wskazaną przez Python — i linię wyżej
IndentationError mieszanie spacji i tabów, złe wcięcie bloku używaj wyłącznie spacji (4) lub wyłącznie tabów
NameError użycie niezdefiniowanej zmiennej lub literówka sprawdź nazwę zmiennej, czy jest zdefiniowana przed użyciem
TypeError operacja na złym typie: "5" + 3, wywołanie nie-funkcji sprawdź typy zmiennych przez type(), dodaj konwersję
ValueError int("abc"), math.sqrt(-1) waliduj dane przed konwersją, użyj try/except
IndexError indeks poza zakresem listy sprawdź czy indeks < len(lista), użyj if lista
KeyError brak klucza w słowniku użyj dict.get() lub if key in dict
ZeroDivisionError dzielenie przez 0 sprawdź mianownik przed dzieleniem: if b != 0
AttributeError metoda nie istnieje dla danego typu, zmienna to None sprawdź typ zmiennej, czy nie jest None przed wywołaniem
RecursionError brak warunku bazowego lub nieskończona rekurencja dodaj/sprawdź warunek bazowy, czy problem maleje
FileNotFoundError błędna ścieżka do pliku, plik nie istnieje sprawdź ścieżkę, użyj os.path.exists()
UnicodeDecodeError złe kodowanie pliku dodaj encoding="utf-8" do open()
Przykłady — błędy i naprawy
# TypeError — dodawanie różnych typów
wiek = input("Wiek: ")
# print(wiek + 1)  ← TypeError: str + int
print(int(wiek) + 1)  # naprawa: konwersja

# IndexError — poza zakresem
lista = [1, 2, 3]
# print(lista[5])  ← IndexError
if len(lista) > 5:
    print(lista[5])

# KeyError — brak klucza
d = {"a": 1, "b": 2}
# print(d["c"])  ← KeyError
print(d.get("c", "brak"))  # naprawa: get()

# AttributeError — None zamiast obiektu
def znajdz(lista, x):
    for elem in lista:
        if elem == x:
            return elem
    # Brak return → zwraca None

wynik = znajdz([1,2,3], 9)
# print(wynik.upper())  ← AttributeError: None
if wynik is not None:
    print(wynik)

# ZeroDivisionError
a, b = 10, 0
# print(a / b)  ← ZeroDivisionError
print(a / b if b != 0 else "dzielenie przez 0")

3. Obsługa wyjątków — try / except / else / finally

Blok try/except pozwala elegancko obsłużyć błąd zamiast crashować program. Używaj go gdy błąd jest możliwy ale nie pewny.

  • try — kod który może rzucić wyjątek
  • except NazwaWyjatku — obsłuż konkretny wyjątek
  • except Exception as e — złap każdy wyjątek i dostęp do jego opisu
  • else — wykonaj gdy try zakończyło się BEZ wyjątku
  • finally — wykonaj zawsze — z wyjątkiem i bez
  • Można łapać wiele wyjątków: except (TypeError, ValueError)
Nie łap wszystkiego przez except Exception Złapanie każdego wyjątku przez gołe except: lub except Exception ukrywa błędy których nie przewidziałeś. Łap konkretne wyjątki — tylko te które spodziewasz się obsłużyć.
Przykład — pełna struktura try/except
def bezpieczne_dzielenie(a, b):
    try:
        wynik = a / b
    except ZeroDivisionError:
        print("Błąd: dzielenie przez zero!")
        return None
    except TypeError as e:
        print(f"Błąd typu: {e}")
        return None
    else:
        # Wykonuje się gdy NIE było wyjątku
        print(f"Sukces: {a} / {b} = {wynik:.4f}")
        return wynik
    finally:
        # Wykonuje się ZAWSZE
        print("Operacja zakończona.")

bezpieczne_dzielenie(10, 3)
bezpieczne_dzielenie(10, 0)
bezpieczne_dzielenie(10, "a")

# Wielokrotne wczytywanie aż do poprawnej wartości
while True:
    try:
        n = int(input("Podaj liczbę całkowitą: "))
        break   # sukces — wyjdź z pętli
    except ValueError:
        print("To nie jest liczba całkowita!")

4. Techniki debugowania

Debugowanie to systematyczne szukanie i naprawianie błędów. Masz kilka narzędzi — od prostych printów do wbudowanego debuggera.

  • Print debugging — wstawiaj print() w kluczowych miejscach
  • type() i isinstance() — sprawdzaj typy zmiennych
  • assert — sprawdź założenie — rzuci AssertionError gdy fałszywe
  • pdb — wbudowany debugger Pythona
  • IDE breakpoints — zatrzymaj program w danym miejscu
  • Testowanie częściowe — uruchamiaj fragmenty kodu osobno
Metoda bisekcji Gdy nie wiesz gdzie jest błąd — dodaj print w połowie kodu. Jeśli wypisuje poprawną wartość — błąd jest w drugiej połowie. Powtarzaj aż znajdziesz winowajcę.
Techniki debugowania w praktyce
# 1. Print debugging
def sortuj(lista):
    print(f"DEBUG wejście: {lista}")
    n = len(lista)
    for i in range(n-1):
        for j in range(n-1-i):
            print(f"DEBUG porównuję [{j}]={lista[j]}"
                  f" z [{j+1}]={lista[j+1]}")
            if lista[j] > lista[j+1]:
                lista[j], lista[j+1] = \
                lista[j+1], lista[j]
    print(f"DEBUG wyjście: {lista}")
    return lista

# 2. type() do sprawdzenia typów
x = input("Podaj liczbę: ")
print(f"Typ przed konwersją: {type(x)}")
x = int(x)
print(f"Typ po konwersji:    {type(x)}")

# 3. assert — sprawdź założenia
def pierwiastek(n):
    assert n >= 0, f"n musi być >= 0, got {n}"
    return n ** 0.5

# 4. Debugger pdb
import pdb
def buggy():
    x = 10
    pdb.set_trace()   # zatrzymaj tu
    y = x * 2
    return y
# W debuggerze: n=następna linia, p x=print x, c=continue

5. Typowe błędy logiczne — program działa ale daje złe wyniki

Błędy logiczne nie powodują wyjątków — program się uruchamia, ale wynik jest nieprawidłowy. To najtrudniejszy rodzaj błędów.

  • Off-by-one — pętla o jeden za dużo lub za mało (range(n) vs range(n+1))
  • Zła kolejność operacjix = x + 1 przed czy po warunku?
  • Alias listyb = a to nie kopia! Modyfikujesz oryginał.
  • Zmienna poza zakresem — globalna zmieniona przez przypadek
  • Błąd zaokrąglenia — float to nie dokładna arytmetyka
  • Zła inicjalizacja — min=0 zamiast min=lista[0]
Off-by-one — najczęstszy błąd Pętle, zakresy, indeksy — zawsze sprawdzaj skrajne przypadki. Dla n=1 czy pętla wykonuje się raz? Czy ostatni element jest uwzględniony? Czy range(n) to 0..n-1 czy 0..n?
Przykłady błędów logicznych i napraw
# BŁĄD 1: Off-by-one
# Chcemy: suma od 1 do 10
suma = 0
for i in range(10):    # ← 0..9, pomija 10!
    suma += i
print(suma)   # 45, nie 55

# NAPRAWA:
suma = sum(range(1, 11))   # 55 ✓

# BŁĄD 2: Alias listy
a = [1, 2, 3]
b = a           # b to ALIAS, nie kopia!
b.append(99)
print(a)        # [1, 2, 3, 99] — zmienione!

# NAPRAWA:
b = a.copy()    # lub a[:]

# BŁĄD 3: Zła inicjalizacja minimum
def znajdz_min(lista):
    minimum = 0           # ← błąd! co jeśli wszystkie > 0?
    for x in lista:
        if x < minimum:
            minimum = x
    return minimum
# Dla [3,5,2] zwróci 0, nie 2!

# NAPRAWA:
def znajdz_min(lista):
    minimum = lista[0]    # inicjalizuj pierwszym elementem
    for x in lista[1:]:
        if x < minimum:
            minimum = x
    return minimum

# BŁĄD 4: Float — błąd zaokrąglenia
print(0.1 + 0.2 == 0.3)  # False !
print(0.1 + 0.2)          # 0.30000000000000004

# NAPRAWA:
import math
print(math.isclose(0.1 + 0.2, 0.3))  # True

6. Dobre praktyki — kod który nie sprawia problemów

Pisanie kodu odpornego na błędy to umiejętność — nie tylko naprawiać błędy, ale pisać tak żeby ich unikać.

  • Waliduj dane wejściowe — nie zakładaj że użytkownik poda poprawne dane
  • Sprawdzaj puste kolekcjeif lista: przed operacjami
  • Używaj get() dla słowników — zamiast naiwnego d[k]
  • Unikaj zmiennych globalnych — przekazuj przez parametry
  • Testuj na przypadkach brzegowych — n=0, pusta lista, ujemne liczby
  • Pisz małe funkcje — łatwiej testować i debugować
  • Komentuj nieoczywisty kod — wyjaśniaj "dlaczego", nie "co"
Przypadki brzegowe — zawsze sprawdzaj Pusta lista, n=0, n=1, ujemne liczby, zero, bardzo duże wartości, tekst zamiast liczby, duplikaty. Jeśli program działa dla n=5 — sprawdź czy działa dla n=0 i n=1.
Przykłady dobrych praktyk
# Walidacja wejścia — zawsze
def oblicz_srednia(oceny):
    if not oceny:           # pusta lista
        return None
    if not all(isinstance(o, (int, float))
               for o in oceny):
        raise TypeError("Oceny muszą być liczbami")
    return sum(oceny) / len(oceny)

# get() zamiast [] dla słowników
config = {"host": "localhost"}
port = config.get("port", 5000)  # domyślnie 5000

# Sprawdzanie zakresu indeksu
def pobierz(lista, i):
    if 0 <= i < len(lista):
        return lista[i]
    return None   # zamiast IndexError

# Testowanie funkcji
def test_srednia():
    assert oblicz_srednia([4,5,3]) == 4.0
    assert oblicz_srednia([10])    == 10.0
    assert oblicz_srednia([])      is None
    print("Testy OK!")

test_srednia()

# Czytelne nazwy zmiennych
# Źle:
x = [i for i in range(100) if i % 2 == 0]

# Dobrze:
parzyste_do_100 = [n for n in range(100)
                   if n % 2 == 0]

Zadania — znajdź i napraw błąd

Zadanie 1 Błędy w sortowaniu bąbelkowym łatwe

Poniższy kod ma 3 błędy logiczne. Znajdź je, wyjaśnij co jest nie tak i napisz poprawną wersję.

Kod z błędami
def bubble_sort_buggy(lista):
    n = len(lista)
    for i in range(n):            # błąd 1
        for j in range(n - i):    # błąd 2
            if lista[j] > lista[j+1]:
                temp       = lista[j]
                lista[j]   = lista[j+1]
                lista[j+1] = lista[j]  # błąd 3
    return lista

dane = [5, 3, 8, 1, 9, 2]
print(bubble_sort_buggy(dane))
Omówienie błędów i naprawy
  1. Błąd 1: range(n) zamiast range(n-1)
    Zewnętrzna pętla powinna iterować n-1 razy. Dla n=6 pętle muszą wykonać 5 przebiegów — range(n) robi 6, co jest zbędne i może prowadzić do błędu w wewnętrznej pętli.
  2. Błąd 2: range(n-i) zamiast range(n-1-i)
    Wewnętrzna pętla dostęp do lista[j+1]. Gdy j=n-i-1, j+1=n-i — to wyjście poza zakres. Musi być range(n-1-i) żeby j+1 ≤ n-1.
  3. Błąd 3: lista[j+1] = lista[j] zamiast temp
    Klasyczny błąd zamiany przez zmienną tymczasową. Po lista[j] = lista[j+1] oryginalna wartość lista[j] jest nadpisana. Trzecia linia powinna być lista[j+1] = temp.
Zadanie 2 Błędy w pracy z plikiem i słownikiem średnie

Poniższy kod ma błędy które spowodują wyjątki lub złe wyniki. Zidentyfikuj każdy błąd, wyjaśnij jaki wyjątek spowoduje i napisz poprawioną wersję.

Kod z błędami
def analiza_pliku(nazwa):
    wyniki = {}
    f = open(nazwa)                    # błąd 1
    for linia in f:
        czesci = linia.split(",")
        imie   = czesci[0]
        wynik  = int(czesci[1])        # błąd 2
        wyniki[imie] += wynik          # błąd 3
    f.close()

    najlepszy = max(wyniki)            # błąd 4
    return najlepszy

print(analiza_pliku("dane.csv"))
Omówienie błędów i naprawy
  1. Błąd 1: brak encoding i brak with
    open(nazwa) bez encoding="utf-8" może dać UnicodeDecodeError dla polskich znaków. Brak with — plik może nie zostać zamknięty przy wyjątku. Naprawa: with open(nazwa, "r", encoding="utf-8") as f.
  2. Błąd 2: brak strip() przed int()
    czesci[1] może zawierać \n na końcu ostatniej linii lub spacje. int(" 95\n") rzuci ValueError. Naprawa: int(czesci[1].strip()).
  3. Błąd 3: KeyError dla nowego klucza
    wyniki[imie] += wynik — gdy imię pojawia się po raz pierwszy, klucz nie istnieje → KeyError. Naprawa: wyniki[imie] = wyniki.get(imie, 0) + wynik.
  4. Błąd 4: max() na kluczach zamiast wartościach
    max(wyniki) zwraca największy klucz (imię alfabetycznie), nie wartość. Naprawa: max(wyniki, key=wyniki.get) zwraca klucz z największą wartością.

Zadania do samodzielnego rozwiązania

Debugowanie to umiejętność — ćwicz aktywnie szukając błędów zamiast tylko ich unikać.

1★☆☆

Bezpieczny kalkulator

Napisz kalkulator który obsługuje wszystkie możliwe błędy: dzielenie przez 0, zły typ wejścia, nieznane działanie. Użyj try/except dla każdego przypadku osobno.

Wskazówka: zagnieżdżone try/except lub wiele klauzul except, konkretne wyjątki
2★☆☆

Znajdź błąd — Fibonacci

Ta funkcja fib(n) ma błąd — zwraca złe wyniki dla niektórych n. Znajdź go używając printów. Wskazówka: sprawdź fib(0), fib(1), fib(2).

Kod: def fib(n): return n if n < 2 else fib(n-1) + fib(n-3)
3★★☆

Testy jednostkowe

Napisz funkcję testuj(func, wejscie, oczekiwane) która sprawdza wynik funkcji i wypisuje PASS lub FAIL z detalami. Przetestuj nią wszystkie algorytmy z kafelka 10.

Wskazówka: wynik = func(wejscie), porównaj z oczekiwane, wypisz obie wartości przy FAIL
4★★☆

Logowanie błędów do pliku

Napisz dekorator @loguj_bledy który łapie wyjątki funkcji i zapisuje je do pliku errors.log z datą, nazwą funkcji i treścią błędu — zamiast crashować program.

Wskazówka: def dekorator(func): def wrapper(*args): try: ... except Exception as e: ...
5★★☆

Przypadki brzegowe

Dla każdej z funkcji: znajdz_min(lista), srednia(lista), silnia(n) — napisz listę przypadków brzegowych i przetestuj czy funkcja je obsługuje poprawnie.

Wskazówka: pusta lista, jeden element, wszystkie równe, n=0, n ujemne, duże n
6★★★

Naprawa uszkodzonego CSV

Masz plik CSV z "brudnymi" danymi — brakujące wartości, złe typy, duplikaty, puste linie. Napisz program który wczytuje plik, naprawia/odrzuca błędne wiersze i zapisuje oczyszczone dane do nowego pliku z raportem ile wierszy naprawiono.

Wskazówka: try/except przy konwersji każdego pola, loguj odrzucone wiersze, strip() wszędzie