🧩 Menadżery kontekstu
Menadżery kontekstu
Menadżer kontekstu to mechanizm, który pomaga bezpiecznie zarządzać zasobami (np. plikami, połączeniami, blokadami). Najważniejsza zaleta: kod sprzątający wykona się zawsze, nawet gdy w środku bloku wystąpi wyjątek.
Instrukcja with
Instrukcja with upraszcza klasyczny wzorzec try/finally.
Zamiast ręcznie pamiętać o zwalnianiu zasobu, przekazujemy to menadżerowi kontekstu.
with open("plik.txt", "w") as plik:
plik.write("Witaj, świecie!")
Równoważny schemat bez with:
plik = open("plik.txt", "w")
try:
plik.write("Witaj, świecie!")
finally:
plik.close()
Co dokładnie robi with?
W uproszczeniu:
- Tworzy obiekt menadżera kontekstu.
- Woła
__enter__()i przypisuje wynik poas. - Wykonuje kod wewnątrz bloku
with. - Zawsze woła
__exit__(...), niezależnie od tego, czy był wyjątek.
Tworzenie własnych menadżerów kontekstu
Aby utworzyć własny menadżer kontekstu, definiujemy klasę z dwiema metodami:
__enter__()- uruchamiana na początku blokuwith; zwykle przygotowuje zasób i go zwraca,__exit__(exc_type, exc_value, traceback)- uruchamiana na końcu bloku; sprząta zasób.
Przykład:
class PolaczenieBazaDanych:
def __enter__(self):
print("Nawiązywanie połączenia z bazą danych...")
# Symulacja połączenia, np. self.conn = connect_to_database()
self.polaczenie = "Połączenie aktywne"
return self.polaczenie
def __exit__(self, exc_type, exc_value, traceback):
print("Zamykanie połączenia z bazą danych...")
# Symulacja zamknięcia połączenia, np. self.conn.close()
self.polaczenie = None
# Użycie menadżera kontekstu
with PolaczenieBazaDanych() as polaczenie:
print(polaczenie)
# Wykonanie operacji na bazie danych
Obsługa wyjątków w __exit__
Metoda __exit__ otrzymuje:
exc_type- typ wyjątku,exc_value- instancję wyjątku,traceback- ślad wykonania.
Jeśli w __exit__ zwrócisz:
False(albo nic) - wyjątek jest propagowany dalej,True- wyjątek zostaje stłumiony.
class LoggerBledu:
def __enter__(self):
print("Start")
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type:
print(f"Wystąpił wyjątek: {exc_value}")
print("Koniec")
return False # wyjątek NIE jest tłumiony
try:
with LoggerBledu():
raise ValueError("Symulowany błąd!")
except ValueError:
print("Obsłużono wyjątek!")
Kiedy zwracać True w __exit__?
Raczej rzadko. Tłumienie wyjątków bywa przydatne, ale łatwo ukryć błąd. Domyślnie bezpieczniej jest nie tłumić wyjątków.
📝 Zadania
Zadanie 1: FileLock - Blokada pliku
Stwórz menadżer kontekstu FileLock w module python1course.zaj04.file_lock, który zapobiega równoczesnemu dostępowi do pliku.
Blokada działa przez tworzenie pliku lock (np. nazwa_pliku.lock), który sygnalizuje, że zasób jest zajęty.
Wymagania:
-
Klasa
FileLockpowinna przyjmować w konstruktorze:filepath(str) - ścieżka do pliku, który ma być zablokowany,timeout(int, opcjonalne, domyślnie 10) - maksymalny czas oczekiwania na zwolnienie blokady (w sekundach).
-
Metoda
__enter__powinna:- Sprawdzać, czy plik lock już istnieje,
- Jeśli istnieje, czekać na jego zwolnienie (sprawdzanie cykliczne),
- Jeśli po upływie
timeoutlock nadal istnieje, zgłosić wyjątekTimeoutErrorz komunikatem:"Nie można uzyskać blokady pliku {filepath} - timeout", - Jeśli lock nie istnieje lub został zwolniony, utworzyć plik lock.
-
Metoda
__exit__powinna:- Usunąć plik lock, jeśli istnieje,
- Działać poprawnie zarówno przy normalnym zakończeniu, jak i przy wyjątku.
-
Dodatkowe wymagania:
- Użyj modułu
timedo implementacji oczekiwania. - Użyj
pathlib.Pathdo pracy z plikami. - Plik lock powinien mieć nazwę:
{oryginalna_nazwa_pliku}.lock.
- Użyj modułu
Przykład użycia:
from pathlib import Path
import time
# Przykład 1: Normalne użycie
with FileLock("data.txt"):
with open("data.txt", "a") as f:
f.write("Dane\n")
# Plik lock zostanie automatycznie usunięty po wyjściu z bloku with
# Przykład 2: Sprawdzenie czy lock został utworzony
lock_path = Path("data.txt.lock")
with FileLock("data.txt"):
print(f"Czy plik lock istnieje? {lock_path.exists()}") # True
# Operacje na pliku
print(f"Czy plik lock istnieje po wyjściu? {lock_path.exists()}") # False
# Przykład 3: Timeout - jeśli plik jest już zablokowany
# Sztucznie tworzymy plik lock, symulując sytuację gdy inny proces go trzyma:
lock_file = Path("data.txt.lock")
lock_file.touch() # Tworzymy plik lock
# Teraz próbujemy uzyskać lock - to spowoduje TimeoutError,
# bo plik lock już istnieje i nie zostanie zwolniony w ciągu 2 sekund:
with FileLock("data.txt", timeout=2):
# Ten kod się nie wykona, bo TimeoutError zostanie zgłoszony
pass
# Pamiętaj, żeby usunąć sztucznie utworzony plik lock po teście:
lock_file.unlink()
Kryteria zaliczenia zadania
- Lock jest tworzony przy wejściu do
with. - Lock jest usuwany po wyjściu z
with(także przy wyjątku). - Timeout działa poprawnie.
- Implementacja nie zostawia "osieroconych" plików lock po błędzie.
Wskazówki
- Użyj
Path.exists()do sprawdzania istnienia pliku lock. - Użyj
Path.touch()do tworzenia pliku lock. - Użyj
Path.unlink()do usuwania pliku lock. - W pętli oczekiwania sprawdzaj np. co 0.1-0.5 sekundy, czy lock został zwolniony.
- Pamiętaj, że
__exit__jest wywoływana zawsze, nawet przy wyjątku.
Zadanie 2: ReportWriter - menadżer kontekstu w projekcie z zaj03
W projekcie z zaj03 IncidentManager potrafi wyciągać statystyki systemu (get_statistics()).
Dodaj możliwość zapisywania raportu do pliku w bezpieczny sposób - z użyciem własnego menadżera kontekstu.
Stwórz klasę ReportWriter w module python1course.zaj04.report_writer:
- Konstruktor przyjmuje
filepath(str) - ścieżka do pliku raportu. __enter__otwiera plik do zapisu i zwraca obiekt, na którym można wywołaćwrite_report(manager):- zapisuje do pliku wynik
manager.get_statistics(), - każde wywołanie
write_reportdopisuje nowy wpis z aktualnym znacznikiem czasu.
- zapisuje do pliku wynik
__exit__zamyka plik - zawsze, nawet gdywrite_reportrzuci wyjątek.
Przykład użycia:
with ReportWriter("raport.txt") as writer:
writer.write_report(manager)
# plik jest zamknięty, wpis jest zapisany
Następnie połącz oba zadania z zaj04: użyj FileLock razem z ReportWriter, żeby zapis raportu był bezpieczny przed równoczesnym dostępem:
with FileLock("raport.txt"):
with ReportWriter("raport.txt") as writer:
writer.write_report(manager)
Co ćwiczymy tym zadaniem?
- zagnieżdżanie menadżerów kontekstu (
withwwith), - separację odpowiedzialności:
FileLockpilnuje dostępu,ReportWriterpilnuje zapisu, - połączenie nowych narzędzi (
zaj04) z istniejącym kodem domenowym (zaj03).