Le mécanisme __slots__ a été introduit dans Python 2.2 pour répondre à l'important surcoût mémoire associé au modèle d'objet par défaut, qui alloue un tableau de hachage __dict__ par instance pour le stockage dynamique des attributs. Le problème se pose dans les applications à grande échelle où des millions d'objets consomment des centaines de mégaoctets de RAM juste pour la gestion du dictionnaire, créant une pression mémoire et des ratés de cache qui dégradent les performances. La solution consiste à déclarer __slots__ comme une variable de classe contenant un itérable de chaînes, ce qui indique à l'interpréteur de réserver des offsets fixes de tableau C pour les attributs plutôt que de faire des recherches dans le hachage, éliminant ainsi __dict__ et __weakref__ à moins d'une demande explicite.
Cette optimisation réduit l'empreinte mémoire par instance d'environ 40-50 % et accélére l'accès aux attributs en évitant le surcoût de hachage. Elle empêche également la création de __weakref__ à moins d'une inclusion explicite, diminuant encore la taille de l'objet. Cependant, cela introduit une rigidité : les instances ne peuvent pas acquérir de nouveaux attributs dynamiquement, et les hiérarchies de classes doivent maintenir la cohérence des slots pour éviter de revenir silencieusement au stockage dans le dictionnaire.
Nous avons rencontré un goulet d'étranglement mémoire critique lors du développement d'un pipeline d'analyse en temps réel traitant dix millions de paquets réseau par seconde, où chaque paquet était représenté comme un objet Python standard. Le stockage basé sur __dict__ par défaut consommait 12 Go de RAM juste pour le surcoût de l'objet. Cela a entraîné des pauses de collecte des ordures qui violaient notre stricte SLA de latence de 10 ms.
Solution 1 : Enregistrements basés sur le dictionnaire. Nous avons d'abord envisagé de stocker les données du paquet dans de simples instances dict. Cela offrait simplicité et sérialisation JSON sans codecs personnalisés, mais le profilage a révélé que les tables de hachage du dictionnaire nécessitaient toujours 48 octets de surcoût par objet en plus de l'indirection des pointeurs, réduisant l'utilisation de la mémoire de seulement 12 %. Le manque d'encapsulation de méthodes a également dispersé la logique métier à travers des modules utilitaires.
Solution 2 : Tuples nommés. Passer à collections.namedtuple a éliminé les dictionnaires par instance en utilisant la structure de C en arrière-plan des tuples. Bien que cela ait considérablement réduit la mémoire, l'immuabilité nous a empêchés de mettre à jour les horodatages des paquets lors de l'analyse, et l'incapacité d'ajouter des valeurs par défaut ou des méthodes de validation a forcé des modèles d'adaptateur maladroits.
Solution 3 : Classes __slots__. Nous avons refactorisé notre classe Packet pour utiliser un stockage d'attributs fixe :
class Packet: __slots__ = ('src_ip', 'dst_ip', 'payload', 'timestamp') def __init__(self, src_ip, dst_ip, payload, timestamp): self.src_ip = src_ip self.dst_ip = dst_ip self.payload = payload self.timestamp = timestamp def size(self): return len(self.payload)
Cela a préservé notre conception orientée objet tout en éliminant complètement __dict__. Nous avons choisi cette approche car elle équilibré l'efficacité mémoire avec la maintenabilité du code, bien que nous devions inclure explicitement '__weakref__' pour soutenir le cache de référence faible de notre piscine d'objets.
Le résultat. L'empreinte mémoire est tombée à 4,5 Go, permettant au pipeline de fonctionner sur du matériel standard. L'accès aux attributs est devenu 35 % plus rapide grâce à un calcul d'offset direct plutôt qu'à des recherches dans une table de hachage, bien que nous ayons dû refactoriser le code de débogage qui dépendait de __dict__ pour l'injection d'attributs dynamiques.
Comment __slots__ interagit-il avec l'héritage multiple lorsque les classes parentes définissent des mises en page de slots conflictuelles ?
Lorsqu'une classe enfant hérite de plusieurs parents utilisant __slots__, Python exige que la mise en page de slots combinée forme une séquence linéaire cohérente sans noms qui se chevauchent. Si les parents partagent des noms d'attribut dans leurs slots, ou si un parent utilise __slots__ tandis qu'un autre utilise le __dict__ par défaut, l'interpréteur crée un __dict__ pour l'enfant, annulant silencieusement les économies de mémoire. Cela se produit parce que Python construit une table de slots unique en concaténant les slots des parents. Les candidats doivent comprendre que tous les parents devraient idéalement utiliser __slots__, et l'enfant doit déclarer explicitement des slots supplémentaires pour éviter le repli sur le dictionnaire.
Pourquoi le module pickle standard échoue-t-il à reconstruire des objets à slot sans méthodes d'état personnalisées ?
Par défaut, pickle tente de sauvegarder et de restaurer l'état d'un objet via son attribut __dict__. Puisque les classes à slots n'ont pas ce dictionnaire à moins d'être ajouté explicitement, le dépickling soulève une AttributeError lorsque le chargeur essaie d'attribuer à des slots inexistants. La solution nécessite la mise en œuvre de __getstate__ pour retourner un dictionnaire des valeurs de slot et de __setstate__ pour les restaurer, ou d'utiliser le protocole __reduce_ex__. De nombreux candidats négligent que __slots__ change le contrat de mise en page de l'objet, supposant que pickle utilise automatiquement la réflexion sur les descripteurs de slot.
__slots__ empêche-t-il les attributs d'instance d'être ajoutés dynamiquement à l'exécution ?
Oui, mais seulement si aucune classe parente ne fournit un __dict__ et que '__dict__' n'est pas explicitement inclus dans la liste des slots. Les candidats manquent souvent que __slots__ supprime simplement l'attribut __dict__; si une classe de base conserve le stockage de dictionnaire par défaut, les instances peuvent toujours accepter des attributs arbitraires via ce dictionnaire hérité. En outre, les instances à slots restent mutables concernant les attributs existants, et elles peuvent toujours être modifiées au niveau de la classe. La véritable immutabilité nécessite des étapes supplémentaires comme l'écrasement de __setattr__, et ne se limite pas à l'utilisation de __slots__.