Le protocole du module pickle a évolué pour gérer des objets où __init__ a des effets secondaires ou des calculs coûteux. Les premiers protocoles exigeaient d'appeler __init__ lors de la désérialisation, ce qui posait des problèmes avec des ressources telles que des poignées de fichiers ou des connexions à des bases de données. Le protocole 2 a introduit __getnewargs__, et le protocole 4 a étendu cela avec __getnewargs_ex__ pour prendre en charge les arguments nommés, offrant un meilleur contrôle sur la reconstruction des objets.
Lors de la désérialisation des objets, Python doit généralement recréer l'état de l'objet. Si __init__ effectue une validation, ouvre des sockets réseau ou modifie l'état global, le réexécuter pendant la désérialisation peut être incorrect ou inefficace. Le défi consiste à restaurer l'état de l'objet sans déclencher ces effets secondaires d'initialisation, en utilisant uniquement les données stockées pour reconstruire l'instance via le constructeur __new__ de niveau inférieur.
La méthode dunder __getnewargs_ex__ (ou __getnewargs__ pour les anciens protocoles) permet à une classe de retourner un tuple de (args, kwargs) que pickle passe directement à __new__, complètement en contournant __init__. Cette méthode est appelée pendant la phase de reconstruction, et sa valeur de retour dicte comment l'instance est créée à partir des octets sérialisés. Cette approche garantit que l'objet est instancié avec le bon état initial sans invoquer une logique d'initialisation qui pourrait être inappropriée pour un objet restauré.
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): # Opération coûteuse que nous voulons éviter lors de la désérialisation self.socket = create_socket(dsn, timeout) def __getnewargs_ex__(self): # Retourner args et kwargs pour __new__ return ((self.dsn,), {'timeout': self.timeout}) def __getstate__(self): # Ne pas sérialiser le socket return {'dsn': self.dsn, 'timeout': self.timeout} def __setstate__(self, state): self.dsn = state['dsn'] self.timeout = state['timeout'] # Ré-établir le socket si nécessaire, ou laisser pour une initialisation paresseuse # Utilisation conn = DatabaseConnection('postgresql://localhost', timeout=60) serialized = pickle.dumps(conn, protocol=4) restored = pickle.loads(serialized) # __init__ non appelé
Un pipeline de traitement de données met en cache des objets de connexion à Redis qui détiennent des sockets TCP ouverts et des jetons d'authentification. Lors de la sérialisation de ces entrées de cache sur disque pour une persistance entre les redémarrages de l'application, appeler __init__ lors de la désérialisation essaie de créer immédiatement de nouvelles connexions socket, ce qui échoue dans des environnements hors ligne ou crée des fuites de ressources. Ce scénario nécessite une stratégie de sérialisation qui préserve les paramètres de connexion tout en retardant l'établissement effectif du réseau jusqu'à ce que l'application le demande explicitement.
Implémentez __getstate__ pour ne retourner que les paramètres de connexion (hôte, port, auth), et __setstate__ pour définir manuellement les attributs et éventuellement rouvrir la connexion. Cette approche est compatible avec les anciens protocoles pickle et est explicite. Cependant, elle invoque toujours __init__ pendant le processus de désérialisation par défaut, sauf si elle est soigneusement évitée avec __reduce__, ce qui peut déclencher des effets secondaires avant que __setstate__ puisse nettoyer.
Implémentez __reduce__ pour retourner un tuple de (callable, args, state), où le callable est une méthode de classe ou __new__ lui-même. Cela permet un contrôle complet sur la reconstruction, mais est verbeux et nécessite une gestion manuelle du dictionnaire d'état. Cela augmente la complexité du code et le risque de divergences de version entre la structure de la classe et les données sérialisées.
Implémentez __getnewargs_ex__ pour retourner ((host, port), {'auth': token}), permettant à pickle d'appeler __new__(host, port, auth=token) directement tout en contournant __init__. Cette solution a été choisie car elle tire parti des fonctionnalités modernes du protocole 4, sépare proprement la phase de 'création d'une instance vide' de la phase 'initialiser les ressources' et évite le code répétitif de __reduce__. Le résultat est un système de mise en cache robuste où les objets de connexion sont restaurés avec leur configuration intacte mais où les sockets restent fermés jusqu'à ce qu'ils soient explicitement nécessaires, évitant l'épuisement des ressources pendant les opérations de désérialisation en lot.
Pourquoi __getnewargs_ex__ empêche-t-il l'appel de __init__, tandis que __setstate__ seul ne le fait pas ?
Lorsque pickle reconstruit un objet, il vérifie la présence de __getnewargs_ex__ (ou __getnewargs__). Si présent, le désérialiseur appelle __new__(*args, **kwargs) avec les valeurs retournées et applique immédiatement l'état via __setstate__ si disponible, en contournant entièrement __init__. En revanche, sans ces méthodes, pickle utilise le chemin de construction par défaut qui invoque toujours __init__ après __new__. Les candidats supposent souvent que __setstate__ remplace l'initialisation, mais __setstate__ ne fait que corriger l'instance après que __init__ a déjà été exécuté, ce qui est trop tard pour prévenir les effets secondaires.
Que se passe-t-il si __getnewargs_ex__ retourne une valeur qui n'est pas un tuple de deux éléments ?
Le protocole pickle exige strictement que __getnewargs_ex__ retourne un tuple de longueur 2 : (args_tuple, kwargs_dict). S'il retourne un seul tuple d'arguments (comme __getnewargs__), Python lèvera une TypeError lors de la désérialisation car il tentera de décomposer le résultat en __new__(*args, **kwargs). S'il retourne None ou d'autres types, le désérialiseur peut planter ou se comporter de manière imprévisible, ce qui diffère de __getnewargs__ qui s'attend simplement à un tuple d'arguments.
Comment __getnewargs_ex__ interagit-il avec __reduce_ex__ lorsque les deux sont définis ?
__reduce_ex__ est la méthode de protocole de niveau supérieur qui orchestre la sérialisation. Si une classe définit __getnewargs_ex__, __reduce_ex__ (spécifiquement dans le protocole 4+) intègre automatiquement sa valeur de retour dans le tuple de réduction en utilisant l'opcode NEWOBJ_EX. Si les deux sont présents mais que __reduce_ex__ retourne un callable personnalisé ne utilisant pas le chemin de reconstruction standard, il prend le pas, ignorant potentiellement complètement __getnewargs_ex__.