22. Python – Debugging (Debugowanie)


Debugging to proces znajdowania i naprawiania błędów (bugów) w kodzie.

Rodzaje błędów:

  1. Składniowe (Syntax Errors) – błędy w kodzie, program się nie uruchomi
  2. Runtime Errors – błędy podczas działania (wyjątki)
  3. Logiczne – program działa, ale wyniki są błędne

Dlaczego debugging jest ważny?

  • Każdy kod ma bugi
  • Debugowanie to 50%+ czasu programisty
  • Umiejętność szybkiego znajdowania błędów = produktywność

Metody debugowania

1. Print debugging (najprostsza)

def oblicz_srednia(liczby):
    suma = 0
    for liczba in liczby:
        print(f"Dodaję: {liczba}")  # Debug
        suma += liczba
    
    print(f"Suma: {suma}")  # Debug
    print(f"Liczba elementów: {len(liczby)}")  # Debug
    
    srednia = suma / len(liczby)
    print(f"Średnia: {srednia}")  # Debug
    
    return srednia

wynik = oblicz_srednia([10, 20, 30])

Zalety:

  • Proste
  • Szybkie
  • Działa wszędzie

Wady:

  • Trzeba pamiętać o usunięciu printów
  • Nieefektywne dla złożonych problemów

2. Asercje (Assertions)

def podziel(a, b):
    assert b != 0, "Dzielnik nie może być zerem!"
    return a / b

# Asercja sprawdza warunek
# Jeśli False → AssertionError z komunikatem

wynik = podziel(10, 2)  # OK
# wynik = podziel(10, 0)  # AssertionError: Dzielnik nie może być zerem!

Użycie:

  • Sprawdzanie założeń
  • Walidacja stanów wewnętrznych
  • Tylko w fazie rozwoju (wyłączane w produkcji)

Python

def oblicz_srednia(liczby):
    assert len(liczby) > 0, "Lista nie może być pusta!"
    assert all(isinstance(x, (int, float)) for x in liczby), "Wszystkie elementy muszą być liczbami!"
    
    return sum(liczby) / len(liczby)

3. Logging – profesjonalne logowanie

import logging

# Konfiguracja
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def oblicz_srednia(liczby):
    logging.debug(f"Wywołano oblicz_srednia z {liczby}")
    
    if not liczby:
        logging.error("Lista jest pusta!")
        return None
    
    suma = sum(liczby)
    logging.info(f"Suma: {suma}")
    
    srednia = suma / len(liczby)
    logging.info(f"Średnia: {srednia}")
    
    return srednia

wynik = oblicz_srednia([10, 20, 30])

Poziomy logowania:

  • DEBUG – szczegółowe informacje diagnostyczne
  • INFO – potwierdzenie że wszystko działa
  • WARNING – coś nieprzewidzianego, ale program działa
  • ERROR – poważny problem
  • CRITICAL – program może się zatrzymać

4. Debugger PDB (Python Debugger)

PDB to wbudowany debugger w Pythonie.

import pdb

def oblicz_srednia(liczby):
    suma = 0
    
    for liczba w liczby:
        pdb.set_trace()  # ← Breakpoint - program się zatrzyma tutaj
        suma += liczba
    
    return suma / len(liczby)

oblicz_srednia([10, 20, 30])

Podstawowe komendy PDB:

  • l (list) – pokaż kod
  • n (next) – następna linia
  • s (step) – wejdź do funkcji
  • c (continue) – kontynuuj do następnego breakpointa
  • p zmienna – wypisz wartość zmiennej
  • pp zmienna – ładnie wypisz
  • h (help) – pomoc
  • q (quit) – wyjdź

5. Breakpoint() – Python 3.7+

def oblicz_srednia(liczby):
    suma = 0
    
    for liczba in liczby:
        breakpoint()  # ← Nowocześniejszy sposób
        suma += liczba
    
    return suma / len(liczby)

oblicz_srednia([10, 20, 30])

breakpoint() to to samo co pdb.set_trace(), ale lepsze (można łatwo wyłączyć wszystkie).

Debugowanie w IDE

Visual Studio Code

Dokumentacja debuggowania

Interfejs użytkownika debuggera

  • Widok Uruchom i debuguj : wyświetla wszystkie informacje dotyczące uruchamiania, debugowania i zarządzania ustawieniami konfiguracji debugowania.
  • Pasek narzędzi debugowania : zawiera przyciski umożliwiające wykonywanie najczęściej spotykanych czynności debugowania.
  • Konsola debugowania : umożliwia przeglądanie i interakcję z wynikami kodu uruchomionego w debugerze.
  • Pasek boczny debugowania : w trakcie sesji debugowania umożliwia interakcję ze stosem wywołań, punktami przerwania, zmiennymi i zmiennymi obserwowanymi.
  • Menu Uruchom : zawiera najczęstsze polecenia uruchamiania i debugowania.

Rozpocznij sesję debugowania

  • Otwórz plik zawierający kod, który chcesz debugować.
  • Rozpocznij sesję debugowania klawiszem F5 lub wybierz opcję Uruchom i debuguj w widoku Uruchom i debuguj ( workbench.view.debug).

W przypadku bardziej złożonych scenariuszy debugowania, takich jak dołączanie do działającego procesu, należy utworzyć 
launch.jsonplik określający konfigurację debugera. Więcej informacji na temat 
konfiguracji debugowania znajdziesz tutaj .

  • Z listy dostępnych debugerów wybierz debuger, którego chcesz użyć.VS Code próbuje uruchomić i debugować aktualnie aktywny plik. W przypadku Node.js, VS Code sprawdza, czy startpackage.jsonpliku znajduje się skrypt, aby określić punkt wejścia aplikacji.
  • Po rozpoczęciu sesji debugowania wyświetlany jest panel DEBUG CONSOLE z wynikami debugowania, a pasek stanu zmienia kolor (domyślny motyw kolorystyczny jest pomarańczowy).

Podstawowe funkcje:

Po rozpoczęciu sesji debugowania u góry okna pojawia się pasek narzędzi Debugowanie. Pasek zawiera akcje sterujące przebiegiem sesji debugowania, takie jak przechodzenie przez kod krok po kroku, wstrzymywanie wykonywania i zatrzymywanie sesji debugowania.

  • F9 – ustaw/usuń breakpoint
  • F5 – rozpocznij debugowanie
  • F10 – następna linia (step over)
  • F11 – wejdź do funkcji (step into)
  • Shift+F11 – wyjdź z funkcji (step out)
  • F5 – kontynuuj

Punkty przerwania

Punkt przerwania to specjalny znacznik, który wstrzymuje wykonywanie kodu w określonym momencie, umożliwiając sprawdzenie stanu aplikacji w tym momencie. VS Code obsługuje kilka typów punktów przerwania

Ustawianie punktów przerwania

Aby ustawić lub usunąć punkt przerwania, kliknij margines edytora lub użyj klawisza F9 w bieżącym wierszu.

  • Punkty przerwania na marginesie edytora są zazwyczaj wyświetlane jako wypełnione na czerwono kółka.
  • Wyłączone punkty przerwania są oznaczone wypełnionym szarym okręgiem.
  • Po uruchomieniu sesji debugowania punkty przerwania, których nie można zarejestrować w debugerze, zmieniają się w szare, puste kółko. To samo może się zdarzyć, jeśli kod źródłowy jest edytowany podczas sesji debugowania bez obsługi edycji na żywo.

Aby mieć większą kontrolę nad punktami przerwania, skorzystaj z sekcji 
„PUNKTY PRZERWANIA” w widoku 
„Uruchom i debuguj” . Ta sekcja zawiera listę wszystkich punktów przerwania w kodzie i udostępnia dodatkowe akcje do zarządzania nimi.

PyCharm

Podobne funkcje:

  • Kliknij na marginesie obok linii → breakpoint
  • Shift+F9 – debuguj
  • F8 – następna linia
  • F7 – wejdź do funkcji

Techniki debugowania

1. Rubber Duck Debugging

Metoda: Wyjaśnij swój kod „gumowej kaczce” (lub koledze) linijka po linijce.

Dlaczego działa: Wymuszając werbalizację często sam znajdziesz błąd.

2. Dziel i zwyciężaj

# Błąd gdzieś w tym kodzie
def skomplikowana_funkcja(dane):
    # 100 linii kodu
    ...
    return wynik

# Podziel na mniejsze części
def czesc1(dane):
    print("Po części 1:", dane)  # Debug
    return wynik1

def czesc2(dane):
    print("Po części 2:", dane)  # Debug
    return wynik2

def czesc3(dane):
    print("Po części 3:", dane)  # Debug
    return wynik3

3. Binary Search Debugging

Jeśli wiesz że błąd jest w 100-liniowej funkcji:

  1. Dodaj print/breakpoint w linii 50
  2. Sprawdź czy błąd jest przed czy po
  3. Powtarzaj aż znajdziesz

4. Zrozum komunikat błędu

Traceback (most recent call last):
  File "program.py", line 10, in <module>
    wynik = oblicz(5, 0)
  File "program.py", line 6, in oblicz
    return a / b
ZeroDivisionError: division by zero

Jak czytać:

  • Od dołu do góry!
  • Ostatnia linia = typ błędu i komunikat
  • Powyżej = stos wywołań (gdzie nastąpił błąd)

Częste błędy i jak je debugować

1. NameError – niezdefiniowana zmienna

print(zmiennna)  # NameError: name 'zmiennna' is not defined

# Debug:
# - Sprawdź literówki
# - Czy zmienna została zdefiniowana?
# - Czy jesteś w odpowiednim zakresie (scope)?

2. IndexError – indeks poza zakresem

lista = [1, 2, 3]
print(lista[5])  # IndexError: list index out of range

# Debug:
print(f"Długość listy: {len(lista)}")
print(f"Próbuję dostać się do indeksu: 5")
# Aha! 5 >= 3

3. KeyError – brak klucza w słowniku

slownik = {"klucz1": "wartość1"}
print(slownik["klucz2"])  # KeyError: 'klucz2'

# Debug:
print(f"Dostępne klucze: {slownik.keys()}")
# Użyj get() zamiast []
print(slownik.get("klucz2", "Domyślna"))

4. TypeError – niewłaściwy typ

wynik = "5" + 5  # TypeError: can only concatenate str (not "int") to str

# Debug:
print(f"Typ pierwszej wartości: {type('5')}")  # str
print(f"Typ drugiej wartości: {type(5)}")      # int
# Konwertuj:
wynik = "5" + str(5)  # lub int("5") + 5

5. AttributeError – brak atrybutu

lista = [1, 2, 3]
lista.add(4)  # AttributeError: 'list' object has no attribute 'add'

# Debug:
print(f"Typ obiektu: {type(lista)}")
print(f"Dostępne metody: {dir(lista)}")
# Poprawka:
lista.append(4)

Narzędzia pomocnicze

1. traceback – szczegółowe informacje o błędzie

import traceback

try:
    wynik = 10 / 0
except Exception as e:
    print("Wystąpił błąd:")
    traceback.print_exc()
    
    # Lub zapisz do pliku
    with open('error.log', 'w') as f:
        traceback.print_exc(file=f)

2. sys.exc_info() – informacje o wyjątku

import sys

try:
    wynik = 10 / 0
except:
    exc_type, exc_value, exc_traceback = sys.exc_info()
    print(f"Typ: {exc_type}")
    print(f"Wartość: {exc_value}")
    print(f"Traceback: {exc_traceback}")

3. inspect – inspekcja obiektów

import inspect

def moja_funkcja(a, b):
    return a + b

# Informacje o funkcji
print(inspect.getsource(moja_funkcja))  # Kod źródłowy
print(inspect.signature(moja_funkcja))  # Sygnatura: (a, b)

4. dis – disassembler

import dis

def dodaj(a, b):
    return a + b

# Zobacz bytecode Pythona
dis.dis(dodaj)

Praktyczne przykłady debugowania

Przykład 1: Błąd logiczny

# Kod ma błąd - znajdź go!
def oblicz_srednia_ocen(oceny):
    suma = 0
    licznik = 0
    
    for ocena in oceny:
        suma += ocena
        licznik += 1
    
    return suma / licznik  # Dzielenie przez zero?

# Test
print(oblicz_srednia_ocen([4, 5, 3]))  # Działa
print(oblicz_srednia_ocen([]))  # ZeroDivisionError!

# Debug z printami
def oblicz_srednia_ocen_debug(oceny):
    print(f"Wejście: {oceny}")
    print(f"Długość: {len(oceny)}")
    
    suma = 0
    licznik = 0
    
    for ocena in oceny:
        suma += ocena
        licznik += 1
        print(f"Suma: {suma}, Licznik: {licznik}")
    
    if licznik == 0:
        print("UWAGA: Dzielenie przez zero!")
        return None
    
    return suma / licznik

# Poprawiona wersja
def oblicz_srednia_ocen_fix(oceny):
    if not oceny:
        return 0  # lub raise ValueError("Lista ocen jest pusta")
    
    return sum(oceny) / len(oceny)

Przykład 2: Debugowanie z PDB

import pdb

def faktoryzacja(n):
    """Zwraca dzielniki liczby n."""
    dzielniki = []
    
    for i in range(1, n + 1):
        pdb.set_trace()  # Zatrzymaj się tutaj
        if n % i == 0:
            dzielniki.append(i)
    
    return dzielniki

print(faktoryzacja(12))

# W PDB:
# p i           # Wypisz i
# p n           # Wypisz n
# p n % i       # Wypisz resztę z dzielenia
# n             # Następna iteracja

Przykład 3: Logging w aplikacji

import logging

# Konfiguracja
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('app.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.balance = balance
        logger.info(f"Utworzono konto dla {owner} z saldem {balance}")
    
    def deposit(self, amount):
        if amount <= 0:
            logger.warning(f"Próba wpłaty nieprawidłowej kwoty: {amount}")
            return False
        
        self.balance += amount
        logger.info(f"{self.owner}: Wpłata {amount}, nowe saldo: {self.balance}")
        return True
    
    def withdraw(self, amount):
        if amount > self.balance:
            logger.error(f"{self.owner}: Próba wypłaty {amount}, ale saldo to tylko {self.balance}")
            return False
        
        self.balance -= amount
        logger.info(f"{self.owner}: Wypłata {amount}, nowe saldo: {self.balance}")
        return True

# Użycie
konto = BankAccount("Jan Kowalski", 1000)
konto.deposit(500)
konto.withdraw(2000)  # Błąd - za mało środków
konto.withdraw(200)

Najlepsze praktyki debugowania

1. Czytaj dokumentację

Przed debugowaniem sprawdź dokumentację funkcji/biblioteki.

2. Pisz testy jednostkowe

import unittest

def dodaj(a, b):
    return a + b

class TestDodaj(unittest.TestCase):
    def test_dodatnie(self):
        self.assertEqual(dodaj(2, 3), 5)
    
    def test_ujemne(self):
        self.assertEqual(dodaj(-2, -3), -5)
    
    def test_mieszane(self):
        self.assertEqual(dodaj(-2, 3), 1)

if __name__ == '__main__':
    unittest.main()

Testy pomagają wykrywać bugi wcześnie.

3. Używaj type hints

def dodaj(a: int, b: int) -> int:
    return a + b

# IDE od razu wykryje błąd
# dodaj("a", "b")  # Typ niezgodny!

4. Code review

Poproś kogoś o przejrzenie kodu – świeże oko często widzi błędy.

5. Git bisect – znajdź kiedy pojawił się bug

git bisect start
git bisect bad  # Obecna wersja ma bug
git bisect good abc123  # Commit abc123 nie miał buga
# Git automatycznie znajdzie commit który wprowadził błąd

Podsumowanie

Metody debugowania:

  1. Print debugging – proste, szybkie
  2. Assertions – sprawdzanie założeń
  3. Logging – profesjonalne logowanie
  4. PDB/breakpoint() – interaktywny debugger
  5. IDE debugger – najpotężniejsze

Techniki:

  • Rubber duck debugging
  • Dziel i zwyciężaj
  • Binary search
  • Czytaj traceback od dołu

Narzędzia:

  • tracebackinspectdis
  • Testy jednostkowe
  • Type hints

Dobre praktyki:

  • Debuguj systematycznie
  • Dokumentuj znalezione błędy
  • Ucz się na błędach
  • Pisz testy

Pamiętaj: Debugging to umiejętność – im więcej ćwiczysz, tym lepszy się stajesz!