Python crée par défaut un dictionnaire __dict__ pour chaque instance de classe. C’est flexible, pratique, mais coûteux en mémoire quand on instancie des milliers ou des millions d’objets. __slots__ est le mécanisme qui supprime ce dictionnaire et stocke les attributs dans une structure compacte. Gain typique : 40 à 60 % de mémoire en moins par instance.
Ce que fait Python sans slots
Sans rien déclarer, chaque instance porte son propre __dict__ :
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
p = Point(1.0, 2.0)
print(p.__dict__) # {'x': 1.0, 'y': 2.0}
Ce dictionnaire permet d’ajouter des attributs à la volée :
p.z = 3.0 # fonctionne
p.label = "A" # aussi
C’est la flexibilité par défaut de Python. Elle a un coût : un dict Python vide pèse plusieurs centaines d’octets (184 octets sur Python 3.12, 232 sur Python 3.8), et ce coût s’applique à chaque instance.
slots : supprimer le dict
En déclarant __slots__, on indique à Python la liste exacte des attributs qu’une instance peut avoir. Python ne crée plus de __dict__ et utilise des descripteurs internes à la place :
class Point:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
p = Point(1.0, 2.0)
print(hasattr(p, "__dict__")) # False
Tenter d’ajouter un attribut non déclaré lève immédiatement une AttributeError :
p.z = 3.0 # AttributeError: 'Point' object has no attribute 'z'
Mesurer le gain réel
Voici comment comparer concrètement avec tracemalloc :
import tracemalloc
class SansSlots:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
class AvecSlots:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
N = 100_000
tracemalloc.start()
objets_sans = [SansSlots(i, i) for i in range(N)]
_, peak_sans = tracemalloc.get_traced_memory()
tracemalloc.stop()
tracemalloc.start()
objets_avec = [AvecSlots(i, i) for i in range(N)]
_, peak_avec = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"Sans __slots__ : {peak_sans / 1_000_000:.1f} MB")
print(f"Avec __slots__ : {peak_avec / 1_000_000:.1f} MB")
Résultat typique sur Python 3.12 :
Sans __slots__ : 28.3 MB
Avec __slots__ : 9.6 MB
Soit une réduction de l’ordre de 66 %. Le gain exact dépend du nombre d’attributs et de leur type.
Quand utiliser slots
Beaucoup d’instances en mémoire simultanément. Les cas classiques : objets de données pour des simulations, nœuds d’arbre ou de graphe, événements dans un pipeline de traitement, enregistrements chargés en masse depuis une API.
Empêcher l’ajout accidentel d’attributs. Dans les classes de valeur ou les DTOs, interdire les attributs dynamiques évite une catégorie de bugs discrets (faute de frappe sur un nom d’attribut qui crée silencieusement un nouvel attribut au lieu de modifier l’existant).
Accélération de l’accès aux attributs. L’accès via descripteur est légèrement plus rapide que via __dict__. La différence est marginale dans la plupart des cas, mais elle peut s’observer sur des benchmarks serrés.
Les pièges à connaître
Héritage : attention aux classes parentes sans slots
Si une classe parente n’a pas de __slots__, ses instances ont un __dict__, et les sous-classes en héritent, même si elles déclarent leurs propres __slots__ :
class Base:
pass # pas de __slots__ → a un __dict__
class Enfant(Base):
__slots__ = ("x",)
e = Enfant()
e.surprise = "je passe quand même" # fonctionne : Base n'a pas de __slots__, ses instances ont un __dict__ qu'Enfant hérite
Pour un vrai bénéfice mémoire sur toute la hiérarchie, chaque classe doit déclarer ses propres __slots__ et ne pas hériter d’une classe avec __dict__.
Incompatibilité avec certains outils
Plusieurs bibliothèques s’attendent à trouver un __dict__ sur les instances : certains ORMs, des frameworks de sérialisation, des outils de copie profonde ou de pickling selon leur version. Avant d’appliquer __slots__ à une classe utilisée avec des frameworks tiers, tester la compatibilité.
__weakref__ disparaît aussi
Par défaut, les instances avec __slots__ ne supportent plus les références faibles (weakref). Si c’est nécessaire, ajouter explicitement "__weakref__" dans la liste :
class Noeud:
__slots__ = ("valeur", "suivant", "__weakref__")
dataclasses et slots
Depuis Python 3.10, @dataclass accepte slots=True directement :
from dataclasses import dataclass
@dataclass(slots=True)
class Coordonnee:
x: float
y: float
z: float
C’est la façon la plus propre d’utiliser __slots__ sur des classes de données modernes. Pas de déclaration manuelle, pas de désynchronisation possible entre __slots__ et les champs.
Récapitulatif
| Sans slots | Avec slots | |
|---|---|---|
| Stockage des attributs | __dict__ par instance | Descripteurs internes |
| Ajout dynamique d’attributs | Oui | Non |
| Mémoire par instance | Plus élevée | 40 à 60 % moins |
| Accès aux attributs | Légèrement plus lent | Légèrement plus rapide |
| Compatibilité frameworks | Universelle | À vérifier |
| Héritage multi-niveaux | Simple | Nécessite une discipline |
__slots__ n’est pas la première optimisation à appliquer. C’est un outil de précision pour les situations où la mémoire est mesurée et documentée comme un problème réel. Si on instancie des centaines d’objets dans une application web classique, l’impact est négligeable. Si on traite des millions d’objets en mémoire dans un pipeline ou une simulation, c’est l’un des leviers les plus directs disponibles en Python pur.
