PythonprogramowanieProgramista Python

Co powoduje, że przypisania do słownika zwracanego przez `locals()` w **Pythonie** są ignorowane w ciałach funkcji, ale utrzymują się na poziomie modułu?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie.

W CPython, referencyjnej implementacji Pythona, zachowanie locals() różni się w zależności od zakresu wykonania z powodu strategii optymalizacyjnych. Na poziomie modułu locals() zwraca sam słownik globalnej przestrzeni nazw, który jest autorytatywnym miejscem przechowywania zmiennych, więc wszelkie modyfikacje natychmiast odzwierciedlają się w środowisku. Wewnątrz funkcji jednak CPython stosuje optymalizację zwaną „szybkimi lokalnymi”, przechowując zmienne w tablicy C o stałym rozmiarze wskaźników PyObject*, indeksowanej przez bajtkod, a nie w tabeli haszującej. Gdy locals() jest wywoływane wewnątrz funkcji, CPython tworzy nowy słownik i napełnia go kopiując wartości z tej szybkiej lokalnej tablicy, co prowadzi do tymczasowego zrzutu. W konsekwencji, pisanie do tego słownika aktualizuje tylko przejrzyste mapowanie, pozostawiając niezmienioną bazową szybką lokalną tablicę, więc funkcja nadal korzysta z oryginalnych wartości zmiennych.

Sytuacja z życia

Zespół deweloperów budował dynamiczne narzędzie do debugowania, które pozwalało programistom wstrzykiwać tymczasowe zmienne użytkowe w środek zakresu działającej funkcji za pośrednictwem interfejsu zdalnego debugera. Początkowa implementacja uchwyciła locals() w punkcie przerwania, wstrzyknęła obiekty pomocnicze do zwracanego słownika i oczekiwała, że działająca funkcja uzyska dostęp do tych pomocników w kolejnych liniach.

Pierwsze podejście próbowało bezpośrednio zmodyfikować słownik zwrócony przez locals(), działając na założeniu, że był to żywy wskaźnik do przestrzeni nazw funkcji. Zalety: Nie wymagało zmian w sygnaturach funkcji i wydawało się syntaktycznie proste. Wady: Zawiodło w sposób cichy, ponieważ CPython traktuje ten słownik jako tylko do odczytu zrzut szybkiej lokalnej tablicy; zmiany były odrzucane, pozostawiając faktyczne lokalne zmienne niezmienione.

Druga strategia polegała na wstrzykiwaniu tymczasowego stanu do globals() zamiast tego, używając globalnej przestrzeni nazw jako wspólnej tablicy ogłoszeń. Zalety: Ta metoda utrzymywała dane w całej aplikacji i była dostępna wszędzie bez przekazywania argumentów. Wady: Wprowadziła poważne zagrożenia dotyczące bezpieczeństwa wątków, zanieczyściła globalną przestrzeń nazw przejściowymi danymi do debugowania i naruszyła zasady enkapsulacji, eksponując wewnętrzny stan całemu procesowi.

Ostateczne rozwiązanie przeorganizowało funkcje instrumentowane, aby akceptowały explicity context jako argument słownika, przez który debugger mógł przekazać mutowalny stan. Zalety: To podejście jest explicite, bezpieczne dla wątków i działa identycznie we wszystkich implementacjach CPython, PyPy i Jython, przestrzegając zasady Pythona, że to, co eksplicite, jest lepsze od tego, co implicite. Wady: Wymagało zmiany sygnatur funkcji docelowych i miejsc ich wywołania, co wymagało większego początkowego refaktoryzowania niż inne podejścia.

Zespół przyjął strategię przekazywania explicite context. To zlikwidowało zależność od szczegółów implementacji specyficznych dla CPython, zapobiegło zanieczyszczeniu przestrzeni nazw i zaowocowało stabilnym, międzyplatformowym narzędziem do debugowania.

Co często umyka kandydatom

Dlaczego locals() zachowuje się inaczej w obrębie zrozumienia listy w porównaniu do standardowej pętli for na poziomie modułu?

W Pythonie 3 zrozumienia list wprowadzają własny lokalny zasięg, podobnie jak funkcja zagnieżdżona, aby zapobiec wyciekaniu zmiennej pętli do otaczającej przestrzeni nazw. Gdy locals() jest wywoływane wewnątrz zrozumienia, zwraca słownik dla tego tymczasowego zasięgu, a nie dla otaczającej funkcji lub modułu. Co więcej, tak samo jak w zwykłych funkcjach, ten słownik jest zrzutem ze szybkich lokalnych, jeśli zrozumienie zostało zaimplementowane jako osobny obiekt kodu, więc zapisy do niego nie utrzymują się. W przeciwieństwie do tego, na poziomie modułu, locals() jest aliasem dla globals(), który jest aktywnym słownikiem modułu. To rozróżnienie jest kluczowe, ponieważ programiści często zakładają, że zrozumienia dzielą tę samą lokalną przestrzeń nazw, co ich zawierająca blokada, co prowadzi do nieporozumień podczas próby debugowania lub wstrzykiwania zmiennych wewnątrz nich.

Czy można wymusić zapis do szybkich lokalnych poprzez manipulację obiektem ramki za pomocą sys._getframe(), a jakie są zagrożenia?

Zaawansowani użytkownicy mogą uzyskać dostęp do aktualnej ramki wykonania za pomocą sys._getframe() i modyfikować frame.f_locals, co CPython ujawnia jako mapowanie do zapisu. W niektórych wersjach przypisanie do frame.f_locals może wywołać zapis do szybkiej lokalnej tablicy za pomocą wewnętrznych API, takich jak PyFrame_LocalsToFast, ale to zachowanie jest zależne od implementacji, wrażliwe na wersje i nie jest częścią specyfikacji języka. Zagrożenia obejmują uszkodzenie pamięci, jeśli liczniki referencji nie są prawidłowo zarządzane, niespójne zachowanie, w którym optymalizator ignoruje zaktualizowane wartości, ponieważ już je zbuforował w rejestrach lub tablicy, oraz całkowitą awarię w innych implementacjach Pythona, takich jak PyPy, które w ogóle nie korzystają z architektury szybkiej lokalnej tablicy. Poleganie na tej technice wprowadza niedookreślone zachowanie i sprawia, że kod jest niemożliwy do utrzymania w różnych wersjach Pythona.

Jak obecność exec() lub eval() z explicity lokalami wpływa na optymalizację szybkich lokalnych w funkcji?

Jeśli ciało funkcji zawiera wywołanie exec() lub eval(), które odwołuje się do lokalnej przestrzeni nazw, CPython nie może zagwarantować, że zmienne będą dostępne tylko przez zoptymalizowaną szybką lokalną tablicę; wykonywany ciąg może dynamicznie wprowadzać lub usuwać zmienne. Aby to uwzględnić, kompilator wyłącza optymalizację szybkiej lokalnej dla tej funkcji, wracając do przechowywania wszystkich zmiennych lokalnych w standardowym słowniku, który jest konsultowany przy każdym dostępie. W tym „nieoptymalizowanym” trybie locals() zwraca ten rzeczywisty słownik, co czyni go aktywnym, mutowalnym widokiem, w którym zmiany utrzymują się natychmiast. To wyjaśnia, dlaczego kod używający exec() często działa wolniej i dlaczego locals() może wyglądać na „poprawnie” działający (pozwalając na zapisy) w takich funkcjach, podczas gdy w funkcjach zoptymalizowanych tak się nie dzieje.