PythonprogramowanieStarszy programista Python

Przez jaki mechanizm rekonstrukcji moduł **pickle** w **Pythonie** pozwala klasom ominąć `__init__`, dostarczając argumenty bezpośrednio do `__new__`?

Zdaj rozmowy kwalifikacyjne z asystentem AI Hintsage

Odpowiedź na pytanie

Protokół modułu pickle ewoluował, aby radzić sobie z obiektami, w których __init__ ma skutki uboczne lub kosztowne obliczenia. Wczesne protokoły wymagały wywołania __init__ podczas deserializacji, co powodowało problemy z zasobami, takimi jak uchwyty plików czy połączenia z bazą danych. Protokół 2 wprowadził __getnewargs__, a Protokół 4 rozszerzył to o __getnewargs_ex__, aby wspierać argumenty nazwane, co zapewnia lepszą kontrolę nad rekonstrukcją obiektu.

Podczas deserializacji obiektów Python zwykle musi odtworzyć stan obiektu. Jeśli __init__ wykonuje walidacje, otwiera gniazda sieciowe lub zmienia globalny stan, ponowne jego wykonanie podczas deserializacji może być niepoprawne lub nieefektywne. Wyzwaniem jest przywrócenie stanu obiektu bez wywoływania tych skutków ubocznych inicjalizacji, wykorzystując jedynie zapisane dane do rekonstrukcji instancji za pomocą niższego poziomu konstruktora __new__.

Metoda dunder __getnewargs_ex__ (lub __getnewargs__ dla starszych protokołów) pozwala klasie zwrócić krotkę (args, kwargs), którą pickle przekazuje bezpośrednio do __new__, całkowicie pomijając __init__. Ta metoda jest wywoływana podczas fazy rekonstrukcji, a jej wartość zwracana decyduje o tym, jak instancja jest tworzona z zserializowanych bajtów. To podejście zapewnia, że obiekt jest inicjalizowany z poprawnym stanem początkowym bez wywoływania jakiejkolwiek logiki inicjalizacji, która mogłaby być niewłaściwa dla odtworzonego obiektu.

import pickle class DatabaseConnection: def __new__(cls, dsn, timeout=30): instance = super().__new__(cls) instance.dsn = dsn instance.timeout = timeout return instance def __init__(self, dsn, timeout=30): # Kosztowna operacja, którą chcemy pominąć podczas deserializacji self.socket = create_socket(dsn, timeout) def __getnewargs_ex__(self): # Zwróć args i kwargs do __new__ return ((self.dsn,), {'timeout': self.timeout}) def __getstate__(self): # Nie serializuj gniazda return {'dsn': self.dsn, 'timeout': self.timeout} def __setstate__(self, state): self.dsn = state['dsn'] self.timeout = state['timeout'] # Przywróć gniazdo, jeśli potrzebne, lub zostaw na leniwą inicjalizację # Użycie conn = DatabaseConnection('postgresql://localhost', timeout=60) serialized = pickle.dumps(conn, protocol=4) restored = pickle.loads(serialized) # __init__ nie jest wywoływane

Sytuacja z życia wzięta

Pipeline do przetwarzania danych buforuje obiekty połączeń Redis, które utrzymują otwarte gniazda TCP i tokeny uwierzytelniające. Podczas serializacji tych wpisów buforu na dysku dla persystencji między restartami aplikacji, wywołanie __init__ podczas deserializacji próbuje natychmiast stworzyć nowe połączenia gniazdowe, co nie udaje się w środowiskach offline lub powoduje wycieki zasobów. Ta sytuacja wymaga strategii serializacji, która zachowuje parametry połączeń, podczas gdy rzeczywiste nawiązywanie połączenia jest opóźniane aż do momentu, gdy aplikacja wyraźnie tego zażąda.

Zaimplementuj __getstate__, aby zwracał tylko parametry połączenia (host, port, auth), a __setstate__, aby ręcznie ustawiać atrybuty i opcjonalnie ponownie otwierać połączenie. To podejście jest zgodne ze starszymi protokołami pickle i jest explicite. Jednak nadal wywołuje __init__ podczas domyślnego procesu deserializacji, chyba że ostrożnie unikniemy tego za pomocą __reduce__, potencjalnie wywołując skutki uboczne, zanim __setstate__ zdąży posprzątać.

Zaimplementuj __reduce__, aby zwrócić krotkę (callable, args, state), gdzie wywoływalny jest metodą klasy lub samym __new__. To zapewnia pełną kontrolę nad rekonstrukcją, ale jest obszerne i wymaga ręcznego zarządzania słownikiem stanu. Zwiększa to złożoność kodu oraz ryzyko niezgodności wersji między strukturą klasy a zserializowanymi danymi.

Zaimplementuj __getnewargs_ex__, aby zwrócić ((host, port), {'auth': token}), pozwalając pickle na bezpośrednie wywołanie __new__(host, port, auth=token) z pominięciem __init__. To rozwiązanie zostało wybrane, ponieważ wykorzystuje cechy nowoczesnego protokołu 4, wyraźnie oddziela fazę 'tworzenia pustej instancji' od fazy 'inicjalizacji zasobów' i unika zbędnych prac związanych z __reduce__. Rezultatem jest solidny system buforowania, w którym obiekty połączeń są przywracane z intact ich konfiguracją, ale gniazda pozostają zamknięte, aż będą wyraźnie potrzebne, zapobiegając wyczerpaniu zasobów podczas operacji wielokrotnej deserializacji.

Co często umyka kandydatom

Dlaczego __getnewargs_ex__ zapobiega wywołaniu __init__, podczas gdy __setstate__ samodzielnie tego nie robi?

Podczas gdy pickle rekonstruuje obiekt, sprawdza obecność __getnewargs_ex__ (lub __getnewargs__). Jeśli są obecne, deserializator wywołuje __new__(*args, **kwargs) z zwróconymi wartościami i natychmiast stosuje stan za pomocą __setstate__, jeśli jest dostępne, całkowicie pomijając __init__. W przeciwieństwie do tego, bez tych metod pickle stosuje domyślną ścieżkę konstrukcji, która zawsze wywołuje __init__ po __new__. Kandydaci często zakładają, że __setstate__ nadpisuje inicjalizację, ale __setstate__ jedynie poprawia instancję po tym, jak __init__ już został wywołany, co jest za późno na zapobieganie skutkom ubocznym.

Co się stanie, jeśli __getnewargs_ex__ zwróci wartość, która nie jest krotką składającą się z dwóch elementów?

Protokół pickle ściśle wymaga, aby __getnewargs_ex__ zwracał krotkę o długości 2: (args_tuple, kwargs_dict). Jeśli zwróci pojedynczą krotkę argumentów (jak __getnewargs__), Python zgłosi TypeError podczas deserializacji, ponieważ próbuje rozpakować wynik do __new__(*args, **kwargs). Jeśli zwróci None lub inne typy, deserializator może się zawiesić lub zachowywać w sposób nieprzewidywalny, różniąc się od __getnewargs__, który oczekuje tylko krotki argumentów.

Jak __getnewargs_ex__ współpracuje z __reduce_ex__, gdy obie są zdefiniowane?

__reduce_ex__ jest metodą wyższego poziomu, która koordynuje serializację. Jeśli klasa definiuje __getnewargs_ex__, __reduce_ex__ (specjalnie w protokole 4+) automatycznie włącza jej wartość zwracaną do krotki redukcyjnej, używając opcodu NEWOBJ_EX. Jeśli obie są obecne, ale __reduce_ex__ zwraca niestandardowy wywoływalny, który nie korzysta z standardowej ścieżki rekonstrukcji, ma pierwszeństwo, co może całkowicie zignorować __getnewargs_ex__.