Historia pytania
Przed Pythonem 2.5 bloki try...finally oraz try...except były wzajemnie wykluczającymi się blokami składniowymi, co zmuszało programistów do ich niezdarnego zagnieżdżania, aby osiągnąć zarówno obsługę błędów, jak i sprzątanie. PEP 341 zjednoczył te konstrukcje, ustanawiając współczesną gwarancję, że finally wykonuje się bez względu na to, jak kończy się blok try. Ta ewolucja była istotna dla wdrażania niezawodnych wzorców zarządzania zasobami w języku pozbawionym deterministycznych destruktorów.
Problem
Programiści często zakładają, że jawne instrukcje return, break lub continue natychmiast kończą bieżący zasięg, potencjalnie pomijając kod sprzątania, który następuje później. Bez wymuszonego wykonania bloków finally, zasoby takie jak uchwyty plików, połączenia z bazą danych czy zablokowania uzyskane w bloku try mogłyby przeciekać w przypadku wczesnego zwrotu. Prowadzi to do wyczerpania zasobów, zakleszczenia lub uszkodzenia danych w systemach produkcyjnych.
Rozwiązanie
Kompilator Pythona tłumaczy try...finally na konkretne instrukcje bajtowe — SETUP_FINALLY, POP_BLOCK i END_FINALLY — które umieszczają obsługę sprzątania w klatce wykonawczej interpretera. Gdy napotka return, interpreter umieszcza wartość zwracania na stosie wartości, wykonuje instrukcje bajtowe bloku finally, a dopiero potem przetwarza oczekiwany zwrot. Jeśli sam blok finally wykona return lub podniesie wyjątek, ten nowy przepływ sterowania zastępuje oryginalny, co zapewnia pierwszeństwo sprzątania.
def process_file(path): f = open(path, 'r') try: data = f.read() if not data: return None # Finally wciąż się wykonuje! return data.upper() finally: f.close() print("Sprzątanie zakończone")
Opis problemu
Mikrousługa przetwarzająca transakcje finansowe sporadycznie wyczerpywała pulę połączeń z bazą danych pod dużym obciążeniem. Dochodzenie prowadziło do wycieku w funkcji pomocniczej, która pozyskiwała połączenie, sprawdzała pamięć podręczną i wczesnym zwrotem, jeśli wystąpił traf w pamięci podręcznej. Programista umieścił wywołanie conn.close() na końcu funkcji, zakładając, że zawsze zostanie osiągnięte, ale wczesne zwroty całkowicie je omijały.
Rozwiązanie 1: Ręczne duplikowanie sprzątania
Zespół rozważał skopiowanie wywołania conn.close() przed każdą instrukcją return. To zostało odrzucone jako trudne do utrzymania, ponieważ przyszłe modyfikacje mogłyby dodać nowe punkty wyjścia, a zduplikowany kod naruszał zasadę DRY. Dodatkowo, to podejście zwiększało wizualny bałagan i ryzyko błędu ludzkiego podczas konserwacji.
Rozwiązanie 2: Menedżerowie kontekstu
Ocenili refaktoryzację, aby użyć with get_connection() as conn:. Chociaż idiomatyczne, wymagało to modyfikacji zewnętrznej fabryki połączeń w celu natychmiastowego wsparcia protokołu menedżera kontekstu. Ryzyko zmiany kodu wspólnej biblioteki przewyższało korzyści dla hotfixa wymagającego natychmiastowego wdrożenia.
Rozwiązanie 3: Osłona try-finally
Wybrane podejście opakowało logikę połączenia w blok try...finally. Ta minimalna zmiana zapewniła, że conn.close() została wywołana przed jakimkolwiek zwrotem, bez refaktoryzacji zależności. Zapewniło to natychmiastowe bezpieczeństwo i wyraźnie sygnalizowało gwarancję sprzątania przyszłym konserwatorom.
Wynik
Poprawka wyeliminowała przeciek połączenia w ciągu kilku godzin od wdrożenia. Wzorzec został następnie nałożony za pomocą reguł lintingowych dla wszystkich funkcji pozyskujących zasoby w kodzie. To zapobiegło podobnym regresjom i ustabilizowało usługę pod szczytowym obciążeniem.
Czy blok finally może modyfikować lub tłumić wartość zwracania funkcji?
Tak. Jeśli blok finally zawiera własną instrukcję return, zastępuje dowolną wartość produkowaną przez bloki try lub except. Oryginalna wartość zwracana jest całkowicie odrzucana. Dodatkowo, jeśli blok finally podnosi wyjątek, ten wyjątek zastępuje każdy wyjątek lub wartość zwracana z wcześniejszych bloków, skutecznie tłumiąc oryginalny wynik.
Co się stanie z wyjątkiem podniesionym w bloku try, jeśli blok finally również podniesie wyjątek?
Oryginalny wyjątek ginie w wyniku maskowania. Python podnosi wyjątek z bloku finally, a ślad stosu oryginalnego wyjątku zostaje odrzucony, chyba że zostanie jawnie przechwycony. Aby temu zapobiec, bloki finally powinny unikać operacji, które mogą podnosić wyjątki, lub używać zagnieżdżonego try...except wewnątrz bloku finally, aby delikatnie obsługiwać błędy sprzątania przy zachowaniu oryginalnego kontekstu wyjątku.
Czy są okoliczności, w których blok finally jest gwarantowany jako nie do wykonania?
Podczas gdy semantyka języka Pythona gwarantuje wykonanie finally w normalnym przepływie kontrolnym, pewne katastrofalne wydarzenia go pomijają. Jeśli system operacyjny wyśle nieuchwytny sygnał, taki jak SIGKILL, jeśli wywołane zostanie os._exit(), lub jeśli proces Pythona zawiedzie z powodu błędu segmentacji, interpreter natychmiast kończy działanie bez wykonywania oczekujących bloków finally. Dodatkowo, nieskończona pętla lub zakleszczenie w bloku try uniemożliwia dotarcie do klauzuli finally w ogóle.