Witajcie ponownie! W pierwszej części nauczyliście się podstaw wyjątków w Pythonie. Teraz czas na bardziej zaawansowane techniki, które pomogą wam pisać profesjonalny kod.
W tej części omówimy rzeczy, które odróżniają juniora od programisty średniozaawansowanego: własne klasy wyjątków, zarządzanie zasobami, wydajność, debugowanie i profesjonalne logowanie błędów.
Ale nie martwcie się - wszystko wyjaśnię krok po kroku, z praktycznymi przykładami!
Własne klasy wyjątków - kiedy i dlaczego
Dlaczego w ogóle tworzyć własne wyjątki?
Wyobraźcie sobie, że pracujecie w sklepie internetowym. Macie różne typy błędów: problemy z produktami, błędy płatności, problemy z użytkownikami. Gdybyście wszystko obsługiwali zwykłym ValueError lub Exception, to tak jakbyście mieli jeden rodzaj alarmu przeciwpożarowego dla kuchni, łazienki i garażu. Technicznie działa, ale nie wiecie gdzie jest problem i jak zareagować.
Własne klasy wyjątków to jak różne alarmy - jeden piszczy, gdy się coś pali, drugi, gdy ktoś włamuje się do domu, trzeci, gdy woda zalewa piwnicę. Od razu wiecie, co się dzieje i jak reagować.
Podstawowe tworzenie własnych wyjątków
Zacznijmy od prostego przykładu. Każda własna klasa wyjątku musi dziedziczyć po Exception (lub jego podklasie):
class ApplicationError(Exception):
"""Ogólny błąd naszej aplikacji"""
pass
class ValidationError(ApplicationError):
"""Błąd walidacji danych"""
pass
class AuthorizationError(ApplicationError):
"""Użytkownik nie ma uprawnień"""
passCo się tutaj dzieje?
ApplicationErrorto nasz "nadrzędny" błąd – wszystkie inne nasze błędy będą z niego dziedziczyć.ValidationErrorużywamy, gdy ktoś poda złe dane (np. nieprawidłowy email).AuthorizationErrorużywamy, gdy ktoś próbuje zrobić coś, na co nie ma uprawnień.
Dlaczego robimy hierarchię? Bo potem możemy łapać błędy na różnych poziomach ogólności. Możemy złapać wszystkie błędy aplikacji (ApplicationError) lub tylko konkretny typ (ValidationError).
Teraz zobaczmy, jak ich używać:
def check_permissions(user, action):
"""Sprawdza czy użytkownik może wykonać daną akcję"""
if not user.is_logged_in:
raise AuthorizationError("Musisz być zalogowany")
if action == "admin" and not user.is_admin:
raise AuthorizationError("Potrzebujesz uprawnień administratora")
def validate_email(email):
"""Sprawdza czy email jest poprawny"""
if not email:
raise ValidationError("Email nie może być pusty")
if "@" not in email:
raise ValidationError("Email musi zawierać znak @")Kluczowa różnica w obsłudze błędów:
Zamiast tego (z wbudowanymi wyjątkami):
try:
check_permissions(user, "admin")
validate_email(user.email)
execute_admin_operation()
except ValueError: # Za ogólne!
print("Jakiś błąd...") # Nie wiemy jaki
except Exception: # Jeszcze bardziej ogólne!
print("Jakiś inny błąd...") # Kompletnie nie wiemy coMożemy zrobić to (z własnymi wyjątkami):
try:
check_permissions(user, "admin")
validate_email(user.email)
execute_admin_operation()
except AuthorizationError as e:
print(f"Brak uprawnień: {e}")
redirect_to_login() # Konkretna akcja
except ValidationError as e:
print(f"Nieprawidłowe dane: {e}")
show_form_with_error(e) # Inna konkretna akcja
except ApplicationError as e:
print(f"Ogólny błąd aplikacji: {e}")
log_error(e) # Jeszcze inna akcjaWidzicie różnicę? Teraz każdy typ błędu ma swoją konkretną reakcję. To jak różne sygnały w samochodzie – jeden mówi o problemie z silnikiem, drugi o braku paliwa, trzeci o niezapiętych pasach.
Wyjątki z dodatkowymi informacjami
Czasami same komunikaty błędów to za mało. Chcemy przekazać więcej szczegółów, żeby nasz kod mógł inteligentnie zareagować.
Przykład z życia: Gdy formularz ma błąd, chcemy podświetlić konkretne pole, które jest źle wypełnione. Żeby to zrobić, musimy wiedzieć, które pole jest problematyczne.
class FieldValidationError(ValidationError):
"""Błąd walidacji konkretnego pola formularza"""
def __init__(self, message, field_name, value=None):
# Wywołujemy konstruktor klasy nadrzędnej
super().__init__(message)
# Dodajemy nasze własne informacje
self.field_name = field_name # Które pole ma błąd
self.value = value # Jaka była nieprawidłowa wartość
def __str__(self):
# Nadpisujemy, jak błąd ma się wyświetlać
return f"Błąd w polu '{self.field_name}': {self.args[0]}"Co się tutaj dzieje krok po kroku:
__init__– to konstruktor, wywołuje się, gdy tworzymy nowy błąd.super().__init__(message)– wywołujemy konstruktor klasy rodzica, przekazując mu podstawową wiadomość.self.field_name– zapisujemy dodatkową informację o nazwie pola.self.value– zapisujemy wartość, która spowodowała błąd.__str__– definiujemy, jak błąd ma wyglądać, gdy go wyświetlimy.
Podobnie możemy zrobić dla błędów API:
class ApiError(ApplicationError):
"""Błąd komunikacji z API"""
def __init__(self, message, status_code=None, details=None):
super().__init__(message)
self.status_code = status_code # Kod HTTP (404, 500, etc.)
self.details = details or {} # Dodatkowe szczegółyPraktyczne użycie:
def validate_form(data):
"""Sprawdza czy dane z formularza są poprawne"""
if not data.get("name"):
raise FieldValidationError(
"Imię jest wymagane",
field_name="name",
value=data.get("name") # Będzie None lub ""
)
age = data.get("age")
if age and (age < 0 or age > 150):
raise FieldValidationError(
"Wiek musi być między 0 a 150",
field_name="age",
value=age
)Teraz możemy inteligentnie obsłużyć błędy:
try:
validate_form({"name": "", "age": 200})
except FieldValidationError as e:
print(f"Błąd walidacji: {e}")
print(f"Problematyczne pole: {e.field_name}")
print(f"Nieprawidłowa wartość: {e.value}")
# Możemy podświetlić konkretne pole w formularzu!
highlight_field_in_form(e.field_name)
show_error_message(str(e))Dlaczego to jest lepsze? Bo nasz kod może zareagować precyzyjnie. Zamiast ogólnego "coś jest źle", wiemy dokładnie, które pole poprawić i możemy pokazać to użytkownikowi.
Hierarchia wyjątków w praktycznym projekcie
Teraz pokażę wam, jak zaprojektować sensowną hierarchię wyjątków dla większego projektu. Weźmy aplikację sklepu internetowego.
Zasada projektowania: Hierarchia powinna odzwierciedlać to, jak będziecie łapać błędy, a nie strukturę waszego kodu.
# Bazowy wyjątek aplikacji – wszystkie nasze błędy z niego dziedziczą
class ShopError(Exception):
"""Bazowy wyjątek dla wszystkich błędów sklepu"""
pass
# Błędy związane z produktami
class ProductError(ShopError):
"""Błędy związane z produktami"""
pass
class ProductNotFound(ProductError):
"""Produkt o podanym ID nie istnieje"""
def __init__(self, product_id):
self.product_id = product_id
super().__init__(f"Produkt o ID {product_id} nie istnieje")
class OutOfStock(ProductError):
"""Nie ma wystarczającej ilości produktu na stanie"""
def __init__(self, product_id, available, requested):
self.product_id = product_id
self.available = available
self.requested = requested
super().__init__(
f"Produkt {product_id}: dostępne {available}, "
f"potrzebne {requested}"
)
# Błędy związane z płatnościami
class PaymentError(ShopError):
"""Błędy związane z płatnościami"""
pass
class InsufficientFunds(PaymentError):
"""Użytkownik nie ma wystarczających środków"""
pass
class CardError(PaymentError):
"""Problem z kartą płatniczą"""
passDlaczego tak to organizujemy?
ShopError– pozwala złapać wszystkie błędy naszej aplikacji jednymexcept.ProductError– pozwala złapać wszystkie problemy z produktami.PaymentError– pozwala złapać wszystkie problemy z płatnościami.- Konkretne błędy – pozwalają reagować na specyficzne sytuacje.
Praktyczne użycie:
def add_to_cart(product_id, quantity):
"""Dodaje produkt do koszyka"""
product = find_product(product_id)
if not product:
raise ProductNotFound(product_id)
if product.stock < quantity:
raise OutOfStock(product_id, product.stock, quantity)
# Tutaj dodajemy do koszyka...
cart.add(product, quantity)
def process_payment(card, amount):
"""Przetwarza płatność"""
if not card.is_valid():
raise CardError("Karta jest nieważna lub wygasła")
if card.balance < amount:
raise InsufficientFunds(f"Saldo: {card.balance}, potrzeba: {amount}")
# Tutaj przetwarzamy płatność...
card.charge(amount)Różne poziomy obsługi:
def buy_products(cart, card):
"""Główna funkcja kupowania – obsługuje błędy na różnych poziomach"""
try:
# Sprawdzamy dostępność wszystkich produktów
for item in cart:
add_to_cart(item.product_id, item.quantity)
# Przetwarzamy płatność
total = calculate_total(cart)
process_payment(card, total)
print("Zakup zakończony pomyślnie!")
return True
except ProductError as e:
# Wszystkie problemy z produktami
print(f"Problem z produktem: {e}")
return False
except PaymentError as e:
# Wszystkie problemy z płatnościami
print(f"Problem z płatnością: {e}")
return False
except ShopError as e:
# Jakikolwiek inny błąd naszej aplikacji
print(f"Ogólny błąd sklepu: {e}")
return FalseMożemy też łapać bardzo konkretne błędy:
def smart_cart_handler(cart, card):
"""Inteligentna obsługa z konkretnymi reakcjami"""
try:
for item in cart:
add_to_cart(item.product_id, item.quantity)
except ProductNotFound as e:
print(f"Produkt {e.product_id} już nie istnieje")
# Usuń z koszyka i kontynuuj
cart.remove(e.product_id)
except OutOfStock as e:
print(f"Za mało towaru. Dostępne: {e.available}")
# Zaproponuj dostępną ilość
cart.update_quantity(e.product_id, e.available)Wskazówka: Projektujcie hierarchię błędów tak, żeby kod, który je łapie, był czytelny i logiczny. Nie kopiujcie struktury klas – myślcie o tym, jak będziecie reagować na błędy.
Zarządzanie zasobami - context managers
Czym w ogóle są "zasoby" i dlaczego trzeba nimi zarządzać?
Zasoby to rzeczy, które system operacyjny daje waszemu programowi do używania, ale musicie je później "oddać". Przykłady:
- Pliki – system daje wam dostęp, ale musicie je zamknąć.
- Połączenia sieciowe – musicie rozłączyć.
- Połączenia z bazą danych – musicie zamknąć.
- Blokady (locks) – musicie zwolnić.
- Pamięć – czasami musicie ręcznie zwolnić.
Problem: Jeśli nie "oddacie" zasobów, system może się zapchać. To jak wypożyczanie książek z biblioteki – jeśli ich nie oddacie, kończą się książki dla innych.
Problem z zarządzaniem zasobami "ręcznie"
Zobaczmy, jak nie robić:
# ❌ PROBLEMATYCZNY KOD
file = open("data.txt", "w")
file.write("Jakieś dane")
# CO JEŚLI TUTAJ WYSTĄPI BŁĄD?
file.write("Więcej danych")
file.close() # Ta linia może się nigdy nie wykonać!Co może pójść nie tak?
- Jeśli wystąpi błąd między
open()aclose(), plik pozostanie otwarty. - System operacyjny ma limit otwartych plików.
- Po przekroczeniu limitu program przestanie działać.
Bezpieczniejsza wersja (ale upierdliwa):
# ✅ BEZPIECZNIEJSZY, ALE UPIERDLIWY KOD
file = None
try:
file = open("data.txt", "w")
file.write("Jakieś dane")
file.write("Więcej danych")
except Exception as e:
print(f"Błąd: {e}")
finally:
if file: # Sprawdź czy plik został otwarty
file.close() # Zamknij ZAWSZE, nawet przy błędzieProblem z tym podejściem: Musicie pamiętać o pisaniu tego za każdym razem. A programiści to leniwe stworzenia i szybko zapomnicie. Poza tym kod robi się długi i nieczytelny.
Rozwiązanie - składnia with
Context manager (menedżer kontekstu) to mechanizm, który automatycznie "sprząta" za was. To jak automatyczne drzwi – wchodzicie, robicie swoje, wychodzicie, a drzwi same się zamykają.
# ✅ PROSTE I BEZPIECZNE
try:
with open("data.txt", "w") as file:
file.write("Jakieś dane")
file.write("Więcej danych")
# Plik zostanie automatycznie zamknięty, nawet przy błędzie!
except Exception as e:
print(f"Błąd: {e}")Co się dzieje krok po kroku:
with open(...) as file:– otwiera plik i zapamiętuje, że go trzeba zamknąć.- Kod wewnątrz bloku – normalne używanie pliku.
- Koniec bloku – Python automatycznie zamyka plik, nawet jeśli był błąd.
Analogia: To jak inteligentny dom – gdy wychodzicie z pokoju, światła same gasną. Nie musicie pamiętać o wyłączaniu.
Tworzenie własnych context managerów
Czasami potrzebujecie zarządzać własnymi "zasobami". Na przykład – chcecie mierzyć czas wykonania operacji.
Najpierw pokażę wam, jak to działa wewnętrznie:
import time
class TimeTracker:
"""Context manager do mierzenia czasu wykonania"""
def __init__(self, operation_name):
self.operation_name = operation_name
self.start_time = None
def __enter__(self):
"""To się dzieje na wejściu do bloku 'with'"""
print(f"Rozpoczynam: {self.operation_name}")
self.start_time = time.time()
return self # Zwracamy siebie (to idzie do 'as nazwa')
def __exit__(self, exception_type, exception_value, traceback):
"""To się dzieje na wyjściu z bloku 'with'"""
execution_time = time.time() - self.start_time
print(f"Zakończono: {self.operation_name}")
print(f"Czas wykonania: {execution_time:.2f} sekund")
# Jeśli był błąd, możemy go sprawdzić
if exception_type:
print(f"Wystąpił błąd: {exception_value}")
return False # False = nie tłumimy błędu (przekaż go dalej)
return False # Zawsze przekazujemy ewentualne błędy dalejJak używać:
# Normalne użycie
with TimeTracker("Pobieranie danych z API"):
time.sleep(2) # Symulacja długiej operacji
print("Dane pobrane!")
# Wyjście:
# Rozpoczynam: Pobieranie danych z API
# Dane pobrane!
# Zakończono: Pobieranie danych z API
# Czas wykonania: 2.00 sekundZ błędem:
try:
with TimeTracker("Operacja z błędem"):
time.sleep(1)
raise ValueError("Coś poszło nie tak!")
except ValueError as e:
print(f"Złapałem błąd: {e}")
# Wyjście:
# Rozpoczynam: Operacja z błędem
# Zakończono: Operacja z błędem
# Czas wykonania: 1.00 sekund
# Wystąpił błąd: Coś poszło nie tak!
# Złapałem błąd: Coś poszło nie tak!Kluczowe: Czas został zmierzony nawet przy błędzie!
Praktyczny przykład - zarządzanie bazą danych
To jest super użyteczny wzorzec dla baz danych. Chcecie, żeby transakcje automatycznie się zatwierdzały przy sukcesie lub cofały przy błędzie:
import sqlite3
class DatabaseConnection:
"""Context manager dla połączenia z bazą danych"""
def __init__(self, database_name):
self.database_name = database_name
self.connection = None
def __enter__(self):
print(f"Łączę z bazą: {self.database_name}")
self.connection = sqlite3.connect(self.database_name)
return self.connection
def __exit__(self, exception_type, exception_value, traceback):
if self.connection:
if exception_type:
# Był błąd – cofnij wszystkie zmiany
print("Błąd - wycofuję zmiany (ROLLBACK)")
self.connection.rollback()
else:
# Sukces – zapisz wszystkie zmiany
print("Sukces - zapisuję zmiany (COMMIT)")
self.connection.commit()
self.connection.close()
print("Połączenie zamknięte")
return False # Nie tłumimy błędówUżycie – operacje przebiegną pomyślnie:
try:
with DatabaseConnection("shop.db") as database:
cursor = database.cursor()
# Operacja 1: Dodaj produkt
cursor.execute("INSERT INTO products (name, price) VALUES (?, ?)",
("Laptop", 2500))
# Operacja 2: Zmniejsz stan magazynowy
cursor.execute("UPDATE stock SET quantity = quantity - 1 WHERE product_id = ?",
(1,))
print("Obie operacje wykonane")
# Jeśli dojdziemy tutaj – wszystko zostanie zapisane (COMMIT)
except Exception as e:
print(f"Błąd operacji bazodanowej: {e}")
# Wyjście przy sukcesie:
# Łączę z bazą: shop.db
# Obie operacje wykonane
# Sukces - zapisuję zmiany (COMMIT)
# Połączenie zamknięteUżycie – jedna operacja się nie powiedzie:
try:
with DatabaseConnection("shop.db") as database:
cursor = database.cursor()
# Operacja 1: Dodaj produkt – OK
cursor.execute("INSERT INTO products (name, price) VALUES (?, ?)",
("Laptop", 2500))
# Operacja 2: Błąd w SQL!
cursor.execute("INVALID SQL SYNTAX BLAH BLAH")
except Exception as e:
print(f"Błąd operacji bazodanowej: {e}")
# Wyjście przy błędzie:
# Łączę z bazą: shop.db
# Błąd - wycofuję zmiany (ROLLBACK)
# Połączenie zamknięte
# Błąd operacji bazodanowej: near "INVALID": syntax errorKluczowe: Pierwsza operacja (dodanie produktu) została automatycznie cofnięta! To nazywa się transakcja – albo wszystko się udaje, albo nic.
Łatwiejszy sposób – dekorator @contextmanager
Tworzenie pełnych klas do prostych rzeczy to overkill. Python oferuje prostszy sposób:
from contextlib import contextmanager
import tempfile
import os
@contextmanager
def temporary_file(content):
"""Tworzy tymczasowy plik i automatycznie go usuwa"""
# Część "setup" – wykonuje się na wejściu do 'with'
temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False)
try:
temp_file.write(content)
temp_file.close()
# 'yield' to magiczne słowo – tutaj przekazujemy kontrolę do bloku 'with'
yield temp_file.name # To trafi do 'as zmienna'
finally:
# Część "cleanup" – wykonuje się ZAWSZE na wyjściu
if os.path.exists(temp_file.name):
os.unlink(temp_file.name)
print(f"Usunięto tymczasowy plik: {temp_file.name}")Jak to działa:
- Kod przed
yield– to jest "setup", wykonuje się na wejściu. yield nazwa_pliku– tutaj przekazujemy kontrolę do blokuwith;nazwa_plikutrafia do zmiennej poas.- Kod po
yield(wfinally) – to jest "cleanup", wykonuje się zawsze na wyjściu.
Użycie:
with temporary_file("Testowa zawartość pliku") as file_name:
print(f"Tymczasowy plik: {file_name}")
# Możemy go normalnie używać
with open(file_name, 'r') as f:
print(f"Zawartość: {f.read()}")
# Gdy wyjdziemy z tego bloku, plik zostanie automatycznie usunięty
# Wyjście:
# Tymczasowy plik: /tmp/tmpabc123.txt
# Zawartość: Testowa zawartość pliku
# Usunięto tymczasowy plik: /tmp/tmpabc123.txtDlaczego to jest super przydatne? Bo często potrzebujecie tymczasowych plików do testów lub tymczasowych operacji, ale nie chcecie zaśmiecać dysku.
Wydajność wyjątków - kiedy się martwić
Podstawowy fakt: wyjątki to nie darmowa zabawa
Przez lata krążył mit, że wyjątki w Pythonie są mega wolne i nie powinno się ich używać. To nieprawda, ale trzeba wiedzieć, kiedy mogą być problemem.
Główne zasady:
- Bloki
try/exceptsą szybkie (w nowych wersjach Pythona prawie darmowe). - Rzucanie i łapanie wyjątków jest wolne.
- Częste wyjątki = problem wydajnościowy.
Python 3.11+ – rewolucja w wydajności
W Pythonie 3.11 wprowadzono "zero-cost exceptions". Brzmi jak marketing, ale to naprawdę duża zmiana.
Co to znaczy w praktyce:
import timeit
# Test wydajności – Python 3.11+
def without_try():
"""Kod bez try/except"""
data = {"key": "value"}
return data.get("key") # Bezpieczne pobieranie
def with_try():
"""Kod z try/except, ale bez wyjątku"""
data = {"key": "value"}
try:
return data["key"] # Bezpośrednie pobieranie
except KeyError:
return None
# Pomiar czasu dla miliona wykonań
time_without = timeit.timeit(without_try, number=1000000)
time_with = timeit.timeit(with_try, number=1000000)
print(f"Bez try: {time_without:.4f}s")
print(f"Z try: {time_with:.4f}s")
print(f"Różnica: {abs(time_with - time_without):.4f}s")
# W Python 3.11+ wyniki są prawie identyczne!
# Bez try: 0.0523s
# Z try: 0.0531s
# Różnica: 0.0008sWniosek: W nowoczesnym Pythonie możecie używać try/except bez obaw o wydajność, jeśli wyjątki nie występują często.
Kiedy wyjątki są drogie
Problem pojawia się, gdy wyjątki są rzucane często:
import timeit
def test_with_frequent_exceptions():
"""Test, gdy wyjątek występuje za KAŻDYM razem"""
data = [1, 2, 3]
try:
return data[10] # IndexError za każdym razem!
except IndexError:
return None
def test_without_exceptions():
"""Test z unikaniem wyjątku przez sprawdzenie"""
data = [1, 2, 3]
if len(data) > 10: # Szybkie sprawdzenie
return data[10]
return None
# Test – 1 000 000 wykonań
time_with_exception = timeit.timeit(test_with_frequent_exceptions, number=1000000)
time_without_exception = timeit.timeit(test_without_exceptions, number=1000000)
print(f"Z częstymi wyjątkami: {time_with_exception:.4f}s")
print(f"Bez wyjątków: {time_without_exception:.4f}s")
print(f"Wyjątki są {time_with_exception/time_without_exception:.1f}x wolniejsze")
# Typowe wyniki:
# Z częstymi wyjątkami: 0.5068s
# Bez wyjątków: 0.1369s
# Wyjątki są 3.7x wolniejszeSzok, prawda? Gdy wyjątek występuje często, kod może działać kilka razy wolniej!
Praktyczne reguły wydajności
Reguła 1: Używajcie wyjątków dla rzeczy, które rzadko się zdarzają
# ✅ DOBRE – błędy są rzadkie (1% przypadków lub mniej)
def divide(a, b):
if b == 0:
raise ValueError("Nie można dzielić przez zero")
return a / b
# ❌ ZŁE – jeśli często dostajecie nieprawidłowe dane
def process_data_bad(data):
try:
return int(data) # Jeśli 50% danych to śmieci = katastrofa wydajnościowa!
except ValueError:
return None
# ✅ LEPSZE – sprawdzenie najpierw
def process_data_good(data):
if data.isdigit(): # Szybkie sprawdzenie
return int(data)
return NoneKiedy używać którego podejścia:
- Wyjątki – gdy błąd zdarza się rzadko (< 5% przypadków).
- Sprawdzenie – gdy błąd zdarza się często (> 20% przypadków).
- Pomiędzy – zależy od sytuacji; testujcie wydajność.
Reguła 2: EAFP vs LBYL – zasada prawdopodobieństwa
- EAFP = "Easier to Ask for Forgiveness than Permission" (łatwiej prosić o wybaczenie niż o pozwolenie).
- LBYL = "Look Before You Leap" (sprawdź, zanim skoczysz).
# EAFP – dobre, gdy sukces jest prawdopodobny
def get_from_dict_eafp(dictionary, key):
try:
return dictionary[key] # Zakładamy, że klucz istnieje
except KeyError:
return None # Reagujemy na wyjątek
# LBYL – dobre, gdy niepowodzenie jest częste
def get_from_dict_lbyl(dictionary, key):
if key in dictionary: # Sprawdzamy najpierw
return dictionary[key]
return NoneKiedy używać którego:
- EAFP – gdy spodziewacie się sukcesu w 80%+ przypadków.
- LBYL – gdy niepowodzenie jest częste (> 20% przypadków).
Specjalny przypadek – dostęp do plików:
# ✅ ZAWSZE używajcie EAFP dla plików
def read_file(filename):
try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError:
return None
# ❌ NIE sprawdzajcie, czy plik istnieje przed otwarciem
def read_file_bad(filename):
if os.path.exists(filename): # Problem: plik może zniknąć...
with open(filename, 'r') as f: # ...między sprawdzeniem a otwarciem!
return f.read()
return NoneDlaczego? Bo plik może zostać usunięty przez inny proces między sprawdzeniem os.path.exists() a open(). To nazywa się race condition.
Reguła 3: Unikajcie wyjątków w pętlach
# ❌ ZŁE – wyjątki w pętli mogą być drogie
def process_strings_bad(string_list):
results = []
for s in string_list:
try:
results.append(int(s)) # Jeśli połowa to śmieci = wolne!
except ValueError:
pass # Ignoruj nieprawidłowe
return results
# ✅ LEPSZE – odfiltruj najpierw, konwertuj potem
def process_strings_better(string_list):
# Najpierw znajdź, które są poprawne (szybko)
valid_strings = [s for s in string_list if s.isdigit()]
# Potem konwertuj (bez wyjątków)
return [int(s) for s in valid_strings]
# ✅ ALBO użyj generatora z filtrowaniem
def process_strings_generator(string_list):
for s in string_list:
if s.isdigit(): # Sprawdź najpierw
yield int(s) # Konwertuj tylko poprawneTest wydajności (dla ciekawskich):
# Lista mieszanych danych – 50% poprawnych, 50% śmieci
test_data = ["123", "abc", "456", "def", "789"] * 1000
time_bad = timeit.timeit(lambda: list(process_strings_bad(test_data)), number=100)
time_good = timeit.timeit(lambda: process_strings_better(test_data), number=100)
print(f"Z wyjątkami w pętli: {time_bad:.4f}s")
print(f"Z filtrowaniem: {time_good:.4f}s")
print(f"Poprawa: {time_bad/time_good:.1f}x szybciej")
# Typowe wyniki:
# Z wyjątkami w pętli: 0.3261s
# Z filtrowaniem: 0.0466s
# Poprawa: 7.0x szybciej!Debugowanie błędów - czytanie śladów stosu
Dlaczego umiejętność czytania błędów jest tak ważna?
To może brzmieć dziwnie, ale błędy to wasi przyjaciele. Dobre błędy mówią wam dokładnie, co jest nie tak i gdzie. Bardziej problematyczne są sytuacje, gdy program cicho robi coś złego – wtedy ciężko znaleźć problem.
Błąd to jak GPS w samochodzie – mówi wam dokładnie, gdzie jesteście i jak dojechać do celu (czyli do naprawy).
Anatomia traceback – jak czytać komunikaty błędów
Gdy Python napotka błąd, wypisuje traceback (ślad stosu). To mapa pokazująca dokładnie, co się stało:
def function_a():
print("Wchodzę do function_a")
return function_b()
def function_b():
print("Wchodzę do function_b")
return function_c()
def function_c():
print("Wchodzę do function_c")
data_list = [1, 2, 3]
return data_list[10] # Tu będzie błąd!
# Wywołanie, które spowoduje błąd
function_a()Traceback wygląda tak:
Wchodzę do function_a
Wchodzę do function_b
Wchodzę do function_c
Traceback (most recent call last):
File "test.py", line 13, in <module>
function_a()
File "test.py", line 3, in function_a
return function_b()
File "test.py", line 7, in function_b
return function_c()
File "test.py", line 11, in function_c
return data_list[10]
IndexError: list index out of rangeJak to czytać (od dołu do góry!):
IndexError: list index out of range– typ błędu i opis.File "test.py", line 11, in function_c– błąd wystąpił wfunction_c, linia 11.return data_list[10]– konkretna linia, która spowodowała błąd.- Linijki wyżej – ścieżka, jak doszło do błędu:
function_a→function_b→function_c.
Analogia: To jak ślad okruszków w bajce o Jasiu i Małgosi. Traceback pokazuje całą drogę od początku do miejsca, gdzie się "zgubiliście".
Najważniejsze zasady czytania traceback:
- Zaczynajcie od dołu – tam jest główny błąd.
- Szukajcie pierwszej linii ze swojego kodu – często błąd jest w bibliotece, ale przyczyna w waszym kodzie.
- Patrzcie na konkretne linijki kodu – tam są podpowiedzi.
Częste typy błędów i co oznaczają
1. IndexError: list index out of range
my_list = [1, 2, 3]
print(my_list[5]) # Lista ma tylko 3 elementy (indeksy 0, 1, 2)Co to znaczy: Próbujecie dostać się do elementu listy, który nie istnieje.
2. KeyError: 'nonexistent_key'
my_dict = {"a": 1, "b": 2}
print(my_dict["c"]) # Klucz "c" nie istniejeCo to znaczy: Próbujecie dostać się do klucza słownika, który nie istnieje.
3. AttributeError: 'str' object has no attribute 'append'
my_string = "hello"
my_string.append("world") # Stringi nie mają metody append!Co to znaczy: Próbujecie użyć metody, która nie istnieje dla danego typu danych.
4. TypeError: unsupported operand type(s) for +: 'int' and 'str'
result = 5 + "10" # Nie można dodać liczby do stringaCo to znaczy: Próbujecie wykonać operację na niekompatybilnych typach.
Programowe przechwytywanie informacji o błędach
Czasami chcecie programowo obsłużyć błędy – np. zalogować szczegóły do pliku:
import traceback
import sys
def get_detailed_error_info():
"""Pokazuje jak programowo wyciągnąć informacje o błędzie"""
try:
data_list = [1, 2, 3]
print(data_list[10]) # Wywołujemy błąd celowo
except Exception as e:
print("=== SZCZEGÓŁOWE INFORMACJE O BŁĘDZIE ===")
# Typ wyjątku
print(f"Typ błędu: {type(e).__name__}")
# Wiadomość błędu
print(f"Wiadomość: {str(e)}")
# Pełny traceback jako string (przydatne do logowania)
tb_string = traceback.format_exc()
print(f"Pełny traceback:\n{tb_string}")
# Informacje o aktualnej ramce (gdzie wystąpił błąd)
exc_type, exc_value, tb = sys.exc_info()
frame = tb.tb_frame
print(f"Plik: {frame.f_code.co_filename}")
print(f"Funkcja: {frame.f_code.co_name}")
print(f"Linia: {tb.tb_lineno}")
get_detailed_error_info()Dlaczego to jest przydatne? Bo możecie te informacje zapisać do loga, wysłać mailem do adminów lub użyć do automatycznego raportowania błędów.
Debugowanie w praktyce - narzędzia
1. Debugger PDB – "zatrzymaj się i rozejrzyj"
import pdb
def problematic_function(data):
"""Funkcja z błędem, którą chcemy zdebugować"""
result = []
for i, element in enumerate(data):
pdb.set_trace() # STOP! Zatrzymaj się tutaj
processed = element * 2
result.append(processed)
return result
# Gdy uruchomicie ten kod, zatrzyma się w debuggerze
problematic_function([1, 2, 3])Komendy w debuggerze:
n(next) – przejdź do następnej linijki w tej samej funkcji.s(step) – wejdź do wywołanej funkcji.c(continue) – kontynuuj wykonanie.p variable_name– wyświetl wartość zmiennej.pp variable_name– ładnie wyświetl wartość zmiennej.l(list) – pokaż kod wokół aktualnej linii.h(help) – pomoc.q(quit) – wyjście z debuggera.
Przykład sesji debugowania:
> test.py(8)problematic_function()
-> processed = element * 2
(Pdb) p element
1
(Pdb) p i
0
(Pdb) n
> test.py(9)problematic_function()
-> result.append(processed)
(Pdb) p processed
2
(Pdb) p result
[]
(Pdb) c2. Logowanie błędów – zostawianie śladów
Debugger jest super do rozwoju, ale w produkcji potrzebujecie logowania:
import logging
# Konfiguracja logowania – WAŻNE!
logging.basicConfig(
level=logging.DEBUG, # Pokaż wszystkie logi
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('application.log'), # Zapisuj do pliku
logging.StreamHandler() # I wyświetlaj na ekranie
]
)
logger = logging.getLogger(__name__)
def function_with_logging(data):
"""Funkcja z dokładnym logowaniem co się dzieje"""
logger.info(f"Rozpoczynam przetwarzanie {len(data)} elementów")
try:
result = []
for i, element in enumerate(data):
logger.debug(f"Przetwarzam element {i}: {element}")
# Sprawdzamy typ danych
if not isinstance(element, (int, float)):
raise TypeError(f"Element {i} nie jest liczbą: {element} (typ: {type(element)})")
processed = element * 2
logger.debug(f"Element {i} przetworzony: {element} -> {processed}")
result.append(processed)
logger.info(f"Przetwarzanie zakończone pomyślnie. Wynik: {result}")
return result
except Exception as e:
# exc_info=True dodaje pełny traceback do loga!
logger.error(f"Błąd podczas przetwarzania: {e}", exc_info=True)
raise # Przekaż błąd dalej
# Test z dobrymi danymi
function_with_logging([1, 2, 3])
# Test z błędnymi danymi
try:
function_with_logging([1, "abc", 3])
except TypeError:
print("Błąd został obsłużony")Wyjście w logach:
2024-07-27 10:30:15,123 - __main__ - INFO - Rozpoczynam przetwarzanie 3 elementów
2024-07-27 10:30:15,124 - __main__ - DEBUG - Przetwarzam element 0: 1
2024-07-27 10:30:15,124 - __main__ - DEBUG - Element 0 przetworzony: 1 -> 2
2024-07-27 10:30:15,125 - __main__ - DEBUG - Przetwarzam element 1: abc
2024-07-27 10:30:15,125 - __main__ - ERROR - Błąd podczas przetwarzania: Element 1 nie jest liczbą: abc (typ: <class 'str'>)
Traceback (most recent call last):
File "test.py", line 25, in function_with_logging
raise TypeError(f"Element {i} nie jest liczbą: {element} (typ: {type(element)})")
TypeError: Element 1 nie jest liczbą: abc (typ: <class 'str'>)Dlaczego logowanie jest lepsze od print?
- Możecie kontrolować poziom szczegółowości.
- Logi idą do pliku (nie znikają, gdy program się kończy).
- Możecie wysyłać logi do zewnętrznych systemów.
exc_info=Truedodaje pełny traceback automatycznie.
Profesjonalne logowanie błędów
Dlaczego logowanie to nie tylko print() na sterydach
Gdy zaczynaliście programować, pewnie debugowaliście swój kod za pomocą instrukcji print(), jak w poniższym przykładzie:
def my_function(data):
print("Wchodzę do funkcji") # ❌
print(f"Dane: {data}") # ❌
result = process(data)
print(f"Wynik: {result}") # ❌
return resultProblem z print():
- W produkcji zaśmieca output.
- Nie można go wyłączyć bez zmiany kodu.
- Nie ma poziomów ważności.
- Nie zapisuje się automatycznie do plików.
- Trudno filtrować i analizować.
Logowanie to profesjonalny sposób na śledzenie, co robi program. To jak czarna skrzynka w samolocie – zapisuje wszystko, co się dzieje, żebyście mogli później analizować problemy.
Poziomy logowania - mówienie różnymi głosami
Python ma 5 głównych poziomów logowania. Każdy ma inne przeznaczenie:
import logging
# Podstawowa konfiguracja
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
# Różne poziomy logowania
logger.debug("Szczegółowe informacje do debugowania") # DEBUG
logger.info("Ogólne informacje o działaniu programu") # INFO
logger.warning("Ostrzeżenie - coś może być nie tak") # WARNING
logger.error("Błąd - coś poszło nie tak") # ERROR
logger.critical("Krytyczny błąd - program może się zawiesić") # CRITICALKiedy używać którego poziomu:
DEBUG – szczegóły implementacji, wartości zmiennych.
logger.debug(f"Połączenie z bazą danych: {connection_string}") logger.debug(f"Query SQL: {sql_query}") logger.debug(f"Otrzymano {len(results)} wyników")INFO – ważne wydarzenia w aplikacji.
logger.info("Użytkownik [email protected] zalogował się pomyślnie") logger.info("Rozpoczynam przetwarzanie 1000 zamówień") logger.info("Backup bazy danych zakończony pomyślnie")WARNING – coś nietypowego, ale program może działać dalej.
logger.warning("Wolne miejsce na dysku poniżej 10%") logger.warning("API odpowiada wolniej niż zwykle (2.5s zamiast <1s)") logger.warning("Użytkownik próbował 3 razy wpisać złe hasło")ERROR – błąd, który przeszkadza w normalnym działaniu.
logger.error("Nie można wysłać emaila powitalnego do nowego użytkownika") logger.error("Błąd połączenia z zewnętrznym API płatności") logger.error("Nie można zapisać pliku na dysku")CRITICAL – poważny błąd, program może przestać działać.
logger.critical("Brak połączenia z główną bazą danych") logger.critical("Brak wolnego miejsca na dysku") logger.critical("Przekroczony limit pamięci - aplikacja może się zawiesić")
Konfiguracja poziomów - filtrowanie szumu
Możecie ustawić minimalny poziom logowania. Wszystko poniżej tego poziomu będzie ignorowane:
import logging
# Pokaż tylko WARNING i wyżej (ukryj DEBUG i INFO)
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger(__name__)
logger.debug("To NIE zostanie wyświetlone")
logger.info("To też NIE zostanie wyświetlone")
logger.warning("To ZOSTANIE wyświetlone")
logger.error("To też ZOSTANIE wyświetlone")W praktyce:
- Rozwój (development) –
DEBUG(widzicie wszystko). - Testowanie (staging) –
INFO(podstawowe informacje). - Produkcja –
WARNINGlubERROR(tylko problemy).
Strukturalne logowanie błędów - JSON to the rescue
Gdy macie dużo logów, zwykły tekst staje się trudny do analizowania. Lepiej zapisywać w formacie JSON:
import logging
import json
from datetime import datetime
class JsonFormatter(logging.Formatter):
"""Formatter, który zapisuje logi w formacie JSON"""
def format(self, record):
# Tworzymy słownik z informacjami o logu
log_data = {
'timestamp': datetime.utcnow().isoformat(),
'level': record.levelname,
'message': record.getMessage(),
'module': record.module, # W którym module wystąpił
'function': record.funcName, # W której funkcji
'line': record.lineno # W której linii
}
# Jeśli był wyjątek, dodaj szczegóły
if record.exc_info:
log_data['exception'] = {
'type': record.exc_info[0].__name__,
'message': str(record.exc_info[1]),
'traceback': self.formatException(record.exc_info)
}
# Zwróć jako JSON string
return json.dumps(log_data, ensure_ascii=False)
# Konfiguracja loggera z formatem JSON
logger = logging.getLogger('my_application')
handler = logging.FileHandler('application.json')
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
logger.setLevel(logging.INFO)Praktyczny przykład użycia:
def process_order(order_id, products):
"""Przetwarza zamówienie z dokładnym logowaniem"""
logger.info(f"Rozpoczynam przetwarzanie zamówienia {order_id}")
try:
for product in products:
logger.debug(f"Przetwarzam produkt: {product}")
# Walidacja produktu
if product['quantity'] <= 0:
raise ValueError(f"Nieprawidłowa ilość produktu {product['id']}: {product['quantity']}")
if product['price'] <= 0:
raise ValueError(f"Nieprawidłowa cena produktu {product['id']}: {product['price']}")
# Oblicz sumę
total = sum(p['price'] * p['quantity'] for p in products)
logger.info(f"Zamówienie {order_id} przetworzone pomyślnie, suma: {total} zł")
return total
except ValueError as e:
logger.error(f"Błąd walidacji zamówienia {order_id}: {e}", exc_info=True)
raise
except Exception as e:
logger.critical(f"Nieoczekiwany błąd w zamówieniu {order_id}", exc_info=True)
raiseTest:
try:
process_order("ORD-123", [
{"id": "P1", "quantity": 2, "price": 100},
{"id": "P2", "quantity": -1, "price": 50} # Błąd!
])
except ValueError:
print("Błąd walidacji został obsłużony")Przykład zapisu w JSON:
{
"timestamp": "2024-07-27T10:30:15.123456",
"level": "ERROR",
"message": "Błąd walidacji zamówienia ORD-123: Nieprawidłowa ilość produktu P2: -1",
"module": "__main__",
"function": "process_order",
"line": 25,
"exception": {
"type": "ValueError",
"message": "Nieprawidłowa ilość produktu P2: -1",
"traceback": "Traceback (most recent call last):\n File..."
}
}Dlaczego JSON jest lepszy:
- Można łatwo filtrować po poziomie, module, funkcji.
- Zewnętrzne narzędzia mogą automatycznie analizować logi.
- Można eksportować do baz danych lub systemów analitycznych.
Wprowadzenie do Sentry - monitoring błędów na sterydach
Sentry to zewnętrzne narzędzie, które automatycznie zbiera błędy z waszej aplikacji. To jak mieć specjalistę od błędów, który 24/7 pilnuje waszej aplikacji.
Co Sentry robi automatycznie:
- Zbiera wszystkie błędy i wyjątki.
- Grupuje podobne błędy razem.
- Pokazuje, jaki % użytkowników dotyczy błąd.
- Wysyła powiadomienia (email, Slack, SMS), gdy wystąpi nowy błąd.
- Pokazuje dokładny kontekst – który użytkownik, jakie dane, jaki request.
Podstawowa konfiguracja:
import sentry_sdk
from sentry_sdk.integrations.logging import LoggingIntegration
# Integracja z logowaniem
sentry_logging = LoggingIntegration(
level=logging.INFO, # INFO i wyżej jako "breadcrumbs" (ślady)
event_level=logging.ERROR # ERROR i wyżej jako "events" (błędy)
)
# Konfiguracja Sentry
sentry_sdk.init(
dsn="https://[email protected]/projekt", # Dostaniecie z konta Sentry
integrations=[sentry_logging],
traces_sample_rate=1.0, # Monitoruj wydajność
send_default_pii=True # Wysyłaj informacje o użytkowniku
)Dodawanie kontekstu do błędów:
def business_operation(user_id, data):
"""Funkcja biznesowa z bogatym kontekstem dla Sentry"""
# Dodaj informacje o użytkowniku
sentry_sdk.set_user({
"id": user_id,
"email": f"user{user_id}@example.com"
})
# Dodaj własne tagi (do filtrowania błędów)
sentry_sdk.set_tag("operation", "data_processing")
sentry_sdk.set_tag("version", "1.2.3")
sentry_sdk.set_tag("environment", "production")
try:
# Dodaj "breadcrumb" – ślad, co robimy
sentry_sdk.add_breadcrumb(
message="Rozpoczynam walidację danych",
category="validation",
level="info"
)
if not data:
raise ValueError("Brak danych do przetworzenia")
sentry_sdk.add_breadcrumb(
message=f"Przetwarzam {len(data)} elementów",
category="processing",
level="info"
)
# Tutaj symulujemy błąd
result = 10 / 0 # ZeroDivisionError!
except Exception as e:
# Dodaj dodatkowy kontekst specyficzny dla tego błędu
sentry_sdk.set_context("operation_data", {
"element_count": len(data) if data else 0,
"data_type": type(data).__name__,
"sample_data": str(data)[:100] if data else None
})
# Błąd zostanie automatycznie wysłany do Sentry
logging.error("Błąd w operacji biznesowej", exc_info=True)
raiseCo zobaczycie w Sentry po błędzie:
- Dokładny traceback.
- Informacje o użytkowniku (ID, email).
- Tagi do filtrowania.
- Breadcrumbs – co działo się przed błędem.
- Kontekst operacji.
- Środowisko, wersja aplikacji.
- Częstotliwość błędu.
Ręczne raportowanie błędów:
def some_operation():
"""Czasami chcecie raportować błąd, ale kontynuować działanie"""
try:
risky_operation()
except Exception as e:
# Wyślij błąd do Sentry, ale nie przerywaj programu
sentry_sdk.capture_exception(e)
# Program może działać dalej
print("Błąd został zaraportowany, ale program działa dalej")
return None # Lub jakaś domyślna wartośćDlaczego Sentry to must-have w produkcji:
- Dowiadujecie się o błędach, zanim użytkownicy się poskarżą.
- Widzicie, które błędy są najczęstsze (priorytety napraw).
- Macie pełny kontekst – można łatwo odtworzyć błąd.
- Statystyki – czy nowa wersja ma więcej czy mniej błędów.
Łączenie wszystkiego - kompleksowy przykład
Teraz pokażę wam, jak połączyć wszystkie techniki w jednym, praktycznym projekcie. To będzie mini-system zarządzania użytkownikami z własnymi wyjątkami, context managerami, logowaniem i integracją z Sentry.
Co będziemy budować:
- System dodawania/pobierania użytkowników z bazy SQLite.
- Własne wyjątki dla różnych typów błędów.
- Context manager do zarządzania transakcjami bazy.
- Dokładne logowanie wszystkich operacji.
- Integracja z Sentry do monitoringu błędów.
Krok 1: Definiujemy hierarchię wyjątków
import logging
import sqlite3
from contextlib import contextmanager
from datetime import datetime
import sentry_sdk
# Bazowy wyjątek dla naszej aplikacji
class DatabaseError(Exception):
"""Błędy związane z bazą danych"""
pass
class UserNotFound(DatabaseError):
"""Użytkownik o podanym ID nie istnieje"""
def __init__(self, user_id):
self.user_id = user_id
super().__init__(f"Użytkownik {user_id} nie istnieje")
class EmailValidationError(Exception):
"""Błąd walidacji adresu email"""
passDlaczego tak organizujemy wyjątki:
DatabaseError– łapiemy wszystkie problemy z bazą jednymexcept.UserNotFound– specyficzny błąd, możemy zareagować konkretnie.EmailValidationError– problemy z danymi użytkownika.
Krok 2: Context manager dla bazy danych
@contextmanager
def database_connection(database_name):
"""
Context manager dla połączenia z bazą SQLite.
Automatycznie commituje przy sukcesie, rollbackuje przy błędzie.
"""
conn = None
try:
logger.debug(f"Łączę z bazą danych: {database_name}")
conn = sqlite3.connect(database_name)
conn.row_factory = sqlite3.Row # Wyniki jako słowniki zamiast tupli
# Przekaż połączenie do bloku 'with'
yield conn
except sqlite3.Error as e:
# Błąd bazy danych – zaloguj i przekształć na nasz wyjątek
logger.error(f"Błąd bazy danych: {e}", exc_info=True)
if conn:
conn.rollback()
raise DatabaseError(f"Błąd operacji bazodanowej: {e}")
except Exception as e:
# Jakikolwiek inny błąd
logger.error(f"Nieoczekiwany błąd: {e}", exc_info=True)
if conn:
conn.rollback()
raise
else:
# Sukces – zatwierdź wszystkie zmiany
if conn:
conn.commit()
logger.debug("Transakcja zatwierdzona w bazie danych")
finally:
# Zawsze zamknij połączenie
if conn:
conn.close()
logger.debug("Połączenie z bazą zamknięte")Co się tutaj dzieje:
yield conn– przekazujemy połączenie do blokuwith.except sqlite3.Error– łapiemy błędy bazy, robimy rollback.else– kod wykonuje się tylko przy braku błędów – robimy commit.finally– zawsze zamykamy połączenie.
Krok 3: Klasa User z walidacją
class User:
"""Klasa reprezentująca użytkownika z walidacją danych"""
def __init__(self, user_id, name, email):
self.id = user_id
self.name = name
self.email = self._validate_email(email)
self.created_at = datetime.now()
def _validate_email(self, email):
"""Walidacja adresu email – prywatna metoda"""
if not email:
raise EmailValidationError("Email nie może być pusty")
if "@" not in email:
raise EmailValidationError("Email musi zawierać znak @")
if "." not in email.split("@")[1]:
raise EmailValidationError("Email musi mieć poprawną domenę")
return email.lower().strip() # Normalizacja
def __str__(self):
return f"User(id={self.id}, name='{self.name}', email='{self.email}')"Dlaczego walidacja w konstruktorze:
- Obiekt
Userzawsze ma poprawne dane. - Nie możemy przypadkowo stworzyć użytkownika z nieprawidłowym emailem.
- Błędy walidacji są rzucane od razu, a nie później.
Krok 4: Menedżer użytkowników z pełnym logowaniem
# Konfiguracja logowania
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
class UserManager:
"""Klasa do zarządzania użytkownikami z pełnym logowaniem błędów"""
def __init__(self, database_name):
self.database_name = database_name
self._create_tables()
def _create_tables(self):
"""Utwórz tabelę w bazie danych, jeśli nie istnieje"""
try:
with database_connection(self.database_name) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TEXT NOT NULL
)
""")
logger.info("Tabela użytkowników sprawdzona/utworzona")
except DatabaseError:
logger.critical("Nie można utworzyć tabeli użytkowników")
raise
def add_user(self, name, email):
"""Dodaj nowego użytkownika z pełną obsługą błędów"""
try:
# Dodaj kontekst dla Sentry PRZED potencjalnym błędem
sentry_sdk.set_context("user_data", {
"name": name,
"email": email
})
# Walidacja – może rzucić EmailValidationError
user = User(None, name, email)
logger.info(f"Dodaję użytkownika: {name} ({email})")
# Zapis do bazy – może rzucić DatabaseError
with database_connection(self.database_name) as conn:
cursor = conn.execute(
"INSERT INTO users (name, email, created_at) VALUES (?, ?, ?)",
(user.name, user.email, user.created_at.isoformat())
)
user.id = cursor.lastrowid
logger.info(f"Użytkownik dodany pomyślnie z ID: {user.id}")
return user
except EmailValidationError as e:
# Błąd walidacji – to wina danych użytkownika, nie krytyczne
logger.warning(f"Błąd walidacji podczas dodawania użytkownika: {e}")
raise
except sqlite3.IntegrityError as e:
# Sprawdź, czy to błąd duplikatu emaila
if "email" in str(e).lower():
logger.warning(f"Próba dodania istniejącego emaila: {email}")
raise EmailValidationError(f"Email {email} już istnieje w systemie")
# Inny błąd integralności
raise DatabaseError(f"Błąd integralności danych: {e}")
except DatabaseError:
# Błąd bazy danych – poważny problem
logger.error("Błąd bazy danych podczas dodawania użytkownika")
raise
except Exception as e:
# Nieoczekiwany błąd – bardzo poważny
logger.error("Nieoczekiwany błąd podczas dodawania użytkownika", exc_info=True)
sentry_sdk.capture_exception(e) # Wyślij do Sentry
raise
def get_user(self, user_id):
"""Pobierz użytkownika po ID"""
try:
logger.debug(f"Pobieranie użytkownika o ID: {user_id}")
with database_connection(self.database_name) as conn:
cursor = conn.execute(
"SELECT * FROM users WHERE id = ?",
(user_id,)
)
row = cursor.fetchone()
if not row:
raise UserNotFound(user_id)
# Rekonstruuj obiekt User
user = User(row['id'], row['name'], row['email'])
user.created_at = datetime.fromisoformat(row['created_at'])
logger.debug(f"Użytkownik pobrany: {user}")
return user
except UserNotFound:
# Użytkownik nie istnieje – to nie błąd systemu
logger.warning(f"Próba pobrania nieistniejącego użytkownika: {user_id}")
raise
except DatabaseError:
# Błąd bazy – poważny problem
logger.error(f"Błąd bazy podczas pobierania użytkownika {user_id}")
raise
except Exception as e:
# Nieoczekiwany błąd
logger.error(f"Nieoczekiwany błąd podczas pobierania użytkownika {user_id}", exc_info=True)
raiseKrok 5: Przykład użycia z obsługą błędów
if __name__ == "__main__":
# Inicjalizacja menedżera użytkowników
manager = UserManager("users.db")
try:
# ✅ Test 1: Dodanie poprawnego użytkownika
user1 = manager.add_user("Jan Kowalski", "[email protected]")
print(f"✅ Dodano użytkownika: {user1}")
# ✅ Test 2: Pobranie użytkownika
retrieved_user = manager.get_user(user1.id)
print(f"✅ Pobrany użytkownik: {retrieved_user}")
# ❌ Test 3: Próba dodania duplikatu (błąd walidacji)
try:
manager.add_user("Anna Nowak", "[email protected]") # Ten sam email!
except EmailValidationError as e:
print(f"❌ Błąd walidacji (oczekiwany): {e}")
# ❌ Test 4: Próba pobrania nieistniejącego użytkownika
try:
manager.get_user(99999)
except UserNotFound as e:
print(f"❌ Użytkownik nie znaleziony (oczekiwany): {e}")
# ❌ Test 5: Nieprawidłowy email
try:
manager.add_user("Test User", "nieprawidłowy-email")
except EmailValidationError as e:
print(f"❌ Błąd walidacji emaila (oczekiwany): {e}")
except DatabaseError as e:
print(f"💥 Błąd bazy danych: {e}")
except Exception as e:
print(f"💥 Nieoczekiwany błąd: {e}")Przykładowe wyjście:
2024-07-27 15:30:15,123 - __main__ - INFO - Tabela użytkowników sprawdzona/utworzona
2024-07-27 15:30:15,125 - __main__ - INFO - Dodaję użytkownika: Jan Kowalski ([email protected])
2024-07-27 15:30:15,127 - __main__ - INFO - Użytkownik dodany pomyślnie z ID: 1
✅ Dodano użytkownika: User(id=1, name='Jan Kowalski', email='[email protected]')
✅ Pobrany użytkownik: User(id=1, name='Jan Kowalski', email='[email protected]')
2024-07-27 15:30:15,130 - __main__ - WARNING - Próba dodania istniejącego emaila: [email protected]
❌ Błąd walidacji (oczekiwany): Email [email protected] już istnieje w systemie
2024-07-27 15:30:15,132 - __main__ - WARNING - Próba pobrania nieistniejącego użytkownika: 99999
❌ Użytkownik nie znaleziony (oczekiwany): Użytkownik 99999 nie istnieje
❌ Błąd walidacji emaila (oczekiwany): Email musi zawierać znak @Co pokazuje ten przykład:
- Hierarchia wyjątków pozwala łapać błędy na odpowiednim poziomie.
- Context manager automatycznie zarządza transakcjami bazy.
- Logowanie dokumentuje wszystkie operacje i błędy.
- Sentry dostanie powiadomienia o nieoczekiwanych błędach.
- Kod jest czytelny pomimo złożonej obsługi błędów.
Podsumowanie i kolejne kroki
Gratulacje! Przebrnęliście przez naprawdę zaawansowane zagadnienia obsługi błędów w Pythonie. Oto, co teraz wiecie:
Najważniejsze koncepcje z tej części:
1. Własne klasy wyjątków
- Projektujcie hierarchię na podstawie tego, jak będziecie łapać błędy.
- Dodawajcie dodatkowe informacje (pola, kontekst) do wyjątków.
- Używajcie opisowych nazw i komunikatów.
2. Context managers
- Automatyczne zarządzanie zasobami (pliki, bazy, połączenia).
- Gwarancja, że zasoby zostaną zwolnione nawet przy błędach.
- Użyj
@contextmanagerdla prostych przypadków.
3. Wydajność wyjątków
- W Pythonie 3.11+ bloki
try/exceptsą prawie darmowe. - Wyjątki są drogie tylko, gdy rzeczywiście występują.
- Unikajcie częstych wyjątków w pętlach.
4. Debugowanie
- Czytajcie traceback od dołu do góry.
- Używajcie debuggera
pdbdo szczegółowego badania. - Logowanie to inwestycja w przyszłość.
5. Profesjonalne logowanie
- Różne poziomy logowania do różnych celów.
- Format JSON dla łatwej analizy.
- Narzędzia jak Sentry do automatycznego monitoringu.
Kluczowe zasady do zapamiętania:
- "Fail fast, fail clearly" – błędy powinny być rzucane szybko i z jasnymi komunikatami.
- "Use exceptions for exceptional cases" – wyjątki dla rzadkich sytuacji, nie dla normalnego przepływu.
- "Log everything important, debug when needed" – logujcie operacje biznesowe, debugujcie problemy.
- "Design exception hierarchies for how you'll catch them" – nie kopiujcie struktury klas.
Co dalej?
W części trzeciej (dla zaawansowanych) omówimy:
- Wyjątki w programowaniu asynchronicznym (asyncio, aiohttp).
- Obsługę błędów w frameworkach webowych (Django, Flask, FastAPI).
- Testowanie kodu z wyjątkami (pytest, mocking, fixtures).
- Zaawansowane wzorce (Circuit Breaker, Retry, Bulkhead).
- Exception Groups i najnowsze funkcje Python 3.11+.
- Wydajność na poziomie produkcyjnym (profiling, optymalizacja).
Zadanie domowe (dla chętnych):
Spróbujcie zaimplementować prosty system zarządzania produktami w sklepie, używając technik z tego postu:
- Własne wyjątki dla różnych błędów (produkt nie istnieje, brak na stanie).
- Context manager dla operacji na "koszyku".
- Logowanie wszystkich operacji.
- Obsługa błędów na różnych poziomach.
Pamiętajcie: Te techniki mogą wydawać się skomplikowane na początku, ale w większych projektach oszczędzą wam mnóstwo czasu i nerwów. Lepiej napisać trochę więcej kodu teraz, niż przez tygodnie szukać błędów w produkcji.
Do zobaczenia w ostatniej części serii!
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".