Het protocol van de pickle-module is geëvolueerd om objecten te behandelen waarbij __init__ neveneffecten of dure berekeningen heeft. Vroegere protocollen vereisten het aanroepen van __init__ tijdens het unpicklen, wat problemen veroorzaakte met bronnen zoals bestandshandles of databaseverbindingen. Protocol 2 introduceerde __getnewargs__, en Protocol 4 breidde dit uit met __getnewargs_ex__ om sleutelargumenten te ondersteunen, wat fijnere controle over de objectreconstructie mogelijk maakt.
Bij het unpicklen van objecten heeft Python meestal de noodzaak om de objectstatus opnieuw te creëren. Als __init__ validatie uitvoert, netwerkverbindingen opent of de globale status wijzigt, kan het opnieuw uitvoeren ervan tijdens het unpicklen onjuist of ondoeltreffend zijn. De uitdaging is om de status van het object te herstellen zonder deze initialisatie-neveneffecten te activeren, alleen gebruikmakend van de opgeslagen gegevens om de instantie via de lagere __new__-constructor te reconstrueren.
De __getnewargs_ex__ dunder-methode (of __getnewargs__ voor oudere protocollen) stelt een klasse in staat om een tuple van (args, kwargs) terug te geven die pickle rechtstreeks aan __new__ doorgeeft, waarbij __init__ volledig wordt overgeslagen. Deze methode wordt aangeroepen tijdens de reconstructiefase, en de returnwaarde bepaalt hoe de instantie wordt gemaakt van de geserialiseerde bytes. Deze aanpak zorgt ervoor dat het object wordt geïnstantieerd met de juiste initiële staat zonder enige initiële logica aan te roepen die ongepast zou kunnen zijn voor een hersteld object.
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): # Dure operatie die we willen overslaan tijdens unpickle self.socket = create_socket(dsn, timeout) def __getnewargs_ex__(self): # Geef args en kwargs terug voor __new__ return ((self.dsn,), {'timeout': self.timeout}) def __getstate__(self): # Don’t pickle de socket return {'dsn': self.dsn, 'timeout': self.timeout} def __setstate__(self, state): self.dsn = state['dsn'] self.timeout = state['timeout'] # Herstel socket indien nodig, of laat het voor lazy init # Gebruik conn = DatabaseConnection('postgresql://localhost', timeout=60) serialized = pickle.dumps(conn, protocol=4) restored = pickle.loads(serialized) # __init__ wordt niet aangeroepen
Een data-verwerkingspijplijn cachet Redis-verbindingobjecten die open TCP-sockets en authenticatietokens vasthouden. Bij het serialiseren van deze cache-items naar schijf voor persistentie tussen applicatieherstarts, probeert het aanroepen van __init__ tijdens het unpicklen onmiddellijk nieuwe socketverbindingen te maken, waardoor het mislukt in offline omgevingen of hulpbronnenlekken ontstaan. Dit scenario vereist een serialisatiestrategie die verbindingsparameters behoudt, terwijl de daadwerkelijke netwerkoprichting wordt uitgesteld totdat de applicatie dit expliciet vraagt.
Implementeer __getstate__ om alleen de verbindingsparameters (host, poort, auth) terug te geven, en __setstate__ om handmatig attributen in te stellen en desgewenst de verbinding opnieuw te openen. Deze aanpak is compatibel met oudere pickle-protocollen en expliciet. Het roept echter nog steeds __init__ aan tijdens het standaard unpickling-proces, tenzij dit zorgvuldig wordt vermeden met __reduce__, wat mogelijk neveneffecten activeert voordat __setstate__ kan schoonmaken.
Implementeer __reduce__ om een tuple van (callable, args, state) terug te geven, waarbij de callable een klassenmethode of __new__ zelf is. Dit biedt volledige controle over reconstructie, maar is uitvoerig en vereist handmatig beheer van de statusdictionary. Dit verhoogt de codecomplexiteit en het risico op versieverschillen tussen de klassenstructuur en de gepickelde gegevens.
Implementeer __getnewargs_ex__ om ((host, poort), {'auth': token}) terug te geven, waardoor pickle __new__(host, port, auth=token) direct kan aanroepen en __init__ kan omzeilen. Deze oplossing is gekozen omdat deze gebruikmaakt van moderne protocol 4-functies, de fase 'creëer lege instantie' van de fase 'initieer bronnen' zuiver scheidt, en de boilerplate van __reduce__ vermijdt. Het resultaat is een robuust cachesysteem waarbij verbindingobjecten met hun configuratie intact worden hersteld, maar sockets gesloten blijven totdat ze expliciet nodig zijn, waardoor hulpbronuitputting tijdens batch-unpicklingoperaties wordt voorkomen.
Waarom voorkomt __getnewargs_ex__ dat __init__ wordt aangeroepen, terwijl __setstate__ alleen dat niet doet?
Wanneer pickle een object reconstrueert, controleert het op __getnewargs_ex__ (of __getnewargs__). Indien aanwezig, roept de unpickler __new__(*args, **kwargs) aan met de teruggegeven waarden en past onmiddellijk de status toe via __setstate__ indien beschikbaar, waarbij __init__ volledig wordt overgeslagen. In tegenstelling tot deze methoden, gebruikt pickle zonder deze methoden het standaard constructiepad, wat altijd __init__ aanroept na __new__. Kandidaten gaan vaak ervan uit dat __setstate__ de initialisatie overschrijft, maar __setstate__ repareert slechts de instantie nadat __init__ al is uitgevoerd, wat te laat is voor het voorkomen van neveneffecten.
Wat gebeurt er als __getnewargs_ex__ een waarde retourneert die geen tuple van twee elementen is?
Het pickle-protocol vereist strikt dat __getnewargs_ex__ een tuple van lengte 2 retourneert: (args_tuple, kwargs_dict). Als het een enkele tuple van argumenten retourneert (zoals __getnewargs__), zal Python tijdens het unpicklen een TypeError genereren omdat het probeert het resultaat uit te pakken in __new__(*args, **kwargs). Als het None of andere types retourneert, kan de unpickler crashen of onvoorspelbaar gedrag vertonen, wat verschilt van __getnewargs__, dat alleen een tuple van argumenten verwacht.
Hoe interageert __getnewargs_ex__ met __reduce_ex__ wanneer beide zijn gedefinieerd?
__reduce_ex__ is de hoger niveau protocolmethode die serialisatie coördineert. Als een klasse __getnewargs_ex__ definieert, neemt __reduce_ex__ (specifiek in protocol 4+) automatisch de returnwaarde op in de reductietuple met behulp van de NEWOBJ_EX opcode. Als beide aanwezig zijn, maar __reduce_ex__ een aangepaste callable retourneert die het standaard reconstructiepad niet gebruikt, heeft deze voorrang, wat ertoe kan leiden dat __getnewargs_ex__ volledig wordt genegeerd.