Mechanizm obsługi wyjątków Pythona tworzy obiekt traceback, który enkapsuluje cały stos wywołań w momencie wystąpienia wyjątku. Każdy węzeł traceback zawiera atrybut tb_frame, który odnosi się do ramki wykonania, która z kolei przechowuje odniesienia do wszystkich zmiennych lokalnych za pośrednictwem f_locals. Ten projekt zachowuje kontekst wykonania w celach debugowania, umożliwiając inspekcję stanów zmiennych nawet po złapaniu wyjątku. Jednak ponieważ ramki odwołują się do swoich ramkek wywołujących za pomocą f_back, a zmienne lokalne mogą odnosić się do samego obiektu wyjątku, przechowywanie tracebacków w długoterminowych obiektach tworzy cykle odniesienia, które uniemożliwiają zbieranie nieużywanych zasobów.
Historia tego zachowania wynika z potrzeby CPython do wspierania debugowania po wystąpieniu błędu za pomocą modułów takich jak pdb, które wymagają dostępu do pełnego stanu wykonania. Gdy występuje wyjątek, interpreter buduje powiązaną listę obiektów traceback za pomocą atrybutu tb_next, gdzie każdy węzeł wskazuje na obiekt ramki. Problem pojawia się, gdy ten traceback jest przechowywany w zamknięciu lub zmiennej instancji: ramka przechowuje obiekt wyjątku w f_locals, jeśli jest przypisany, podczas gdy wyjątek trzyma traceback za pomocą __traceback__, tworząc cykliczne odniesienie. Rozwiązaniem jest jawne przerwanie tych odniesień za pomocą traceback.clear_frames() lub unikanie przechowywania surowych obiektów traceback, zamiast tego natychmiastowe wyodrębnienie istotnych danych.
import sys import traceback def risky_function(): local_data = "x" * 10**6 # Duży obiekt raise ValueError("Coś poszło nie tak") def handle_error(): try: risky_function() except ValueError: exc_type, exc_val, exc_tb = sys.exc_info() # Przechowywanie exc_tb tworzy cykl odniesienia return exc_tb # Nigdy nie rób tego w produkcji # Scenariusz wycieku pamięci saved_tb = handle_error() # saved_tb.tb_frame.f_locals nadal odnosi się do dużego ciągu # Nawet po powrocie funkcji pamięć nie jest zwalniana
Pipeline przetwarzania danych napotkał poważne wyczerpanie pamięci podczas operacji wsadowych, zużywając 8GB pamięci RAM w ciągu kilku godzin, mimo przetwarzania tylko 1MB fragmentów sekwencyjnie. Dochodzenie ujawniło, że middleware obsługi błędów rejestrował pełne obiekty traceback w globalnym deque do asynchronicznego logowania, z zamiarem późniejszej serializacji. Każdy traceback zachowywał odniesienia do całych ramek stosu zawierających dużą pandas DataFrames i macierze numpy, uniemożliwiając zbieranie nieużywanych zasobów pomimo tego, że funkcje przetwarzania wróciły.
Jednym z rozważanych rozwiązań było natychmiastowe przekształcenie tracebacków na ciągi za pomocą traceback.format_exc(). To podejście całkowicie przerywa odniesienia obiektów, redukując pamięć do bezpiecznych poziomów, ale poświęca możliwość przeprowadzenia strukturalnej analizy zmiennych ramek podczas debugowania. Inną opcją było ręczne wyzerowanie tracebacku za pomocą exc_tb = None po wyodrębnieniu, ale okazało się to kruchym i podatnym na błędy w różnych ścieżkach kodu. Zespół ostatecznie wdrożył traceback.clear_frames(saved_tb) po wyodrębnieniu niezbędnych informacji do debugowania, co jasno wyczyściło zmienne lokalne ze wszystkich ramek w łańcuchu traceback, zachowując odniesienia do numerów linii i obiektów kodu.
To rozwiązanie zmniejszyło zużycie pamięci o 99%, zachowując jednocześnie wystarczający kontekst debugowania. Pipeline teraz przetwarza terabajty danych bez wzrostu pamięci, a system logowania przechowuje usunięte podsumowania tracebacków zamiast obiektów na żywo. Programiści nauczyli się traktować tracebacki jako tymczasowe zasoby, a nie trwałe struktury danych.
Dlaczego sys.exc_info() nadal zwraca aktywne informacje o tracebacku, nawet po opuszczeniu bloku except?
W Pythonie interpreter utrzymuje stan wyjątku w pamięci lokalnej wątku, aż będzie wyraźnie wyczyszczony lub wystąpi nowy wyjątek. Gdy opuszczasz blok except, informacje o wyjątku pozostają dostępne przez sys.exc_info(), ponieważ interpreter nie może wiedzieć, czy przechowałeś odniesienia do tracebacku gdzie indziej. Ten projekt wspiera zagnieżdżoną obsługę wyjątków i pułapki debugowania, ale oznacza, że po prostu opuszczenie zakresu except nie zwalnia ramek. Aby poprawnie wyczyścić ten stan, musisz wywołać sys.exc_info() i usunąć wszystkie trzy zwrócone wartości, lub użyć sys.exc_clear() w Pythonie 2 (przestarzałe w Pythonie 3).
Jak przechowywanie atrybutu __traceback__ wyjątku w zamknięciu tworzy cykl odniesienia, który pokonuje cykliczny zbieracz śmieci?
Gdy przechowujesz exc.__traceback__ w zamknięciu lub atrybucie obiektu, tworzysz cykl: traceback odnosi się do ramek za pośrednictwem tb_frame, ramki odnosi się do zmiennych lokalnych za pomocą f_locals, a jeśli jakakolwiek zmienna lokalna odnosi się do wyjątku (bezpośrednio lub pośrednio), wyjątek odnosi się do tracebacku za pomocą __traceback__. Podczas gdy cykliczny zbieracz śmieci Pythona radzi sobie z czystymi obiektami Pythona, obiekty ramek zawierają wskaźniki na poziomie C i mogą opóźniać zbieranie lub wymagać specyficznych pokoleń. Co więcej, jeśli ramka zawiera metody __del__ lub rozszerzenia C przechowujące zasoby zewnętrzne, cykl staje się nie do zlikwidowania. Przerwanie cyklu wymaga wywołania traceback.clear_frames() lub usunięcia atrybutu __traceback__ wyjątku.
Co odróżnia atrybut tb_next obiektów traceback od atrybutu f_back obiektów ramek w kontekście propagacji wyjątków?
Kandydaci często mylą te dwa łańcuchy. Atrybut tb_next łączy obiekty traceback w porządku rozwiązywania wyjątków, reprezentując ślad stosu od punktu wystąpienia do punktu przechwycenia. W przeciwieństwie do tego, f_back łączy ramki wykonania w bieżącym stosie wywołań, który zmienia się w miarę dalszego działania programu. Gdy wyjątek zostaje złapany, traceback rejestruje zrzut ramek za pośrednictwem tb_frame, ale f_back w tych ramach może nadal wskazywać na aktywne ramki, jeśli nie zostaną prawidłowo odizolowane. Modyfikacja tb_next wpływa tylko na łańcuch historii wyjątku, podczas gdy f_back odzwierciedla dynamiczny stos wywołań, co czyni zrozumienie tego faktu kluczowym, ponieważ tracebacki zachowują historyczny stan, podczas gdy ramki reprezentują bieżące wykonanie.