Accéder au contenu principal

Comment écrire des classes économes en mémoire en Python

Découvrez la gestion de la mémoire en Python et des techniques avancées pour coder des classes économes en mémoire. Mettez en pratique pour des performances optimales.
Actualisé 19 sept. 2026  · 10 min lire

Explorez l'IA

ChatGPTClaudePerplexity

Écrire des classes économes en mémoire en Python est essentiel pour développer des applications robustes, performantes et adaptées à des contraintes systèmes variées. Une utilisation efficace de la mémoire accélère l’exécution et améliore la scalabilité en consommant moins de ressources, un point clé dans les environnements à mémoire limitée.

Dans cet article, je vous présente les fondamentaux de la gestion mémoire en Python et différentes techniques pratiques pour écrire des classes économes en mémoire. Que vous développiez des systèmes complexes ou de simples scripts, ces bonnes pratiques vous aideront à produire un code Python plus propre et plus efficace.

Les fondamentaux de la gestion mémoire en Python

Le système de gestion mémoire de Python est sophistiqué et simplifie le développement en prenant en charge la complexité des opérations mémoire. Ce mécanisme est indispensable pour garantir que les applications Python tournent efficacement.

Comment Python gère la mémoire

Au cœur de la gestion mémoire de Python se trouve le tas privé (private heap), où sont stockés tous les objets et structures de données. Les programmeurs n’y accèdent pas directement : ils manipulent les objets via le système de gestion mémoire de Python.

Ce système repose notamment sur :

  • Des allocateurs mémoire : Python utilise un allocateur intégré pour réserver et libérer des blocs mémoire. Il optimise les petits objets via des « free lists », qui recyclent des blocs déjà alloués pour accélérer les allocations futures. Pour des objets plus complexes (listes, dictionnaires, etc.), Python emploie une allocation dynamique afin de gérer les variations de taille.
  • Des pools mémoire : Python organise la mémoire en pools selon la taille des objets pour limiter la fragmentation et les surcoûts, en regroupant des objets de tailles similaires.

Désallocation et ramasse-miettes

Python combine deux approches pour le ramasse-miettes :

  • Le comptage de références : méthode principale, Python suit le nombre de références de chaque objet. Quand ce compteur tombe à zéro, l’allocateur libère immédiatement la mémoire correspondante.
  • Le collecteur cyclique : Python inclut un collecteur pour gérer les références circulaires, que le comptage de références ne peut pas résoudre seul. Il identifie périodiquement les groupes d’objets qui se référencent mutuellement sans être utilisés ailleurs, puis les nettoie.

Profiler et optimiser l’usage mémoire

Comprendre et optimiser la consommation mémoire est crucial pour préserver les performances d’une application.

Python propose plusieurs outils de profilage mémoire, qui vont au‑delà du débogage standard et aident à repérer les inefficiences :

  • pympler : outil complet qui suit l’usage mémoire et analyse l’espace objet, idéal pour des investigations détaillées.
  • memory_profiler : fournit une analyse ligne par ligne de la consommation mémoire pour localiser précisément les points gourmands.
  • tracemalloc : intégré à la bibliothèque standard, il suit les allocations et détecte les fuites, avec des tendances d’allocation à l’appui.

Nous utiliserons la bibliothèque pympler dans ce tutoriel. Installez‑la avec pip dans votre terminal :

pip install pympler

Pour aller plus loin : Introduction au profilage mémoire en Python.

Techniques pour écrire des classes économes en mémoire en Python

Maintenant que nous avons vu les bases, examinons des techniques avancées pour écrire des classes économes en mémoire.

1. Optimiser la structure des classes avec __slots__

En Python, chaque instance possède un dictionnaire __dict__ pour stocker ses variables d’instance. Cette flexibilité a un coût mémoire important lorsque l’on crée de nombreuses instances. En définissant __slots__, vous demandez à Python d’allouer un ensemble fixe d’attributs, supprimant le besoin du dictionnaire dynamique.

L’usage de __slots__ apporte deux bénéfices majeurs :

  • Moins de mémoire utilisée : sans dictionnaire d’instance, chaque objet occupe moins d’espace. À l’échelle de millions d’instances, l’économie devient significative.
  • Accès plus rapide aux attributs : l’accès via __slots__ est plus direct et évite les opérations de table de hachage propres aux dictionnaires.

Exemple pratique : points dans un plan

Considérons une classe simple représentant un point en deux dimensions :

class PointWithoutSlots:
   def __init__(self, x, y):
       self.x = x
       self.y = y

class PointWithSlots:
   __slots__ = ('x', 'y')
   def __init__(self, x, y):
       self.x = x
       self.y = y

Dans la classe PointWithoutSlots, chaque instance possède un __dict__ pour stocker les attributs x et y, tandis que PointWithSlots n’en a pas, ce qui réduit la mémoire utilisée par instance.

Pour illustrer la différence, créons mille points et mesurons la mémoire totale avec pympler :

from pympler import asizeof

points_without_slots = [PointWithoutSlots(i, i) for i in range(1000)]
points_with_slots = [PointWithSlots(i, i) for i in range(1000)]

print("Total memory (without __slots__):", asizeof.asizeof(points_without_slots))
print("Total memory (with __slots__):", asizeof.asizeof(points_with_slots))

La sortie obtenue est :

Comparaison mémoire avec et sans slots

Comparaison de l’utilisation mémoire avec et sans slots

On constate généralement que les instances de PointWithSlots consomment moins de mémoire que celles de PointWithoutSlots, ce qui illustre l’efficacité de __slots__ pour réduire le surcoût mémoire par instance.

Limites de l’utilisation de __slots__

Bien que bénéfiques, les __slots__ présentent des limites :

  • Moins de flexibilité : une fois définis, il n’est plus possible d’ajouter dynamiquement de nouveaux attributs aux instances.
  • Complexité avec l’héritage : si une classe avec __slots__ est héritée, la sous‑classe doit aussi définir ses __slots__ pour conserver le gain mémoire, ce qui peut compliquer la conception.

Malgré ces limites, __slots__ reste un levier puissant pour optimiser la mémoire, notamment dans des contextes à fortes contraintes ou avec un très grand nombre d’instances.

2. Mettre en œuvre des design patterns économes en mémoire

Certains design patterns favorisent une meilleure utilisation et gestion de la mémoire en Python.

Deux patterns particulièrement efficaces pour la conception orientée classes sont Flyweight et Singleton. Bien utilisés dans des scénarios comportant de nombreuses instances ou des données partagées, ils réduisent drastiquement l’empreinte mémoire.

Le pattern Flyweight

Flyweight consiste à partager les parties communes de l’état entre plusieurs objets plutôt que de dupliquer ces données dans chaque objet, réduisant significativement l’empreinte mémoire globale.

Prenons une application de dessin où chaque cercle possède une couleur, un rayon et des coordonnées. Avec Flyweight, la couleur et le rayon, souvent partagés, peuvent être stockés dans des objets communs :

class Circle:
   # Flyweight object
   def __init__(self, color):
       self.color = color
   def draw(self, radius, x, y):
       print(f"Drawing a {self.color} circle with radius {radius} at ({x},{y})")

class CircleFactory:
   # Factory to manage Flyweight objects
   _circles = {}
   @classmethod
   def get_circle(cls, color):
       if not cls._circles.get(color):
           cls._circles[color] = Circle(color)
       return cls._circles[color]

Ici, plusieurs cercles partagent l’objet Circle fourni par la CircleFactory selon la couleur : la donnée « couleur » n’est stockée qu’une seule fois.

Créons quelques instances pour observer l’économie mémoire.

import sys

factory = CircleFactory()
circle1 = factory.get_circle("Red")
circle2 = factory.get_circle("Red")
circle3 = factory.get_circle("Blue")

print("Memory address of circle1:", id(circle1))
print("Memory address of circle2:", id(circle2))
print("Memory address of circle3:", id(circle3))
print ("-----------------------------")
print("Memory used by circle1:", sys.getsizeof(circle1))
print("Memory used by circle2:", sys.getsizeof(circle2))
print("Memory used by circle3:", sys.getsizeof(circle3))

La sortie obtenue est :

Utilisation mémoire avec le pattern flyweight

Utilisation mémoire pour le pattern Flyweight

On voit que circle1 et circle2 partagent la même adresse mémoire : ils correspondent au même objet (instance partagée pour « Red »). circle3 a une adresse différente car il s’agit d’une autre couleur (« Blue »).

circle1 et circle2 étant la même instance, la CircleFactory partage bien l’objet Circle. Avec mille cercles rouges, une seule instance porterait la couleur, au lieu d’en dupliquer mille : économie mémoire substantielle.

Le pattern Singleton

Singleton garantit qu’une classe ne possède qu’une seule instance et offre un point d’accès global. Idéal pour des configurations ou ressources partagées, il évite de répliquer des objets identiques et réduit ainsi le surcoût mémoire.

Considérez la classe Database suivante :

class Database:
   _instance = None
   def __new__(cls):
       if cls._instance is None:
           cls._instance = super(Database, cls).__new__(cls)
           # Initialize any attributes of the instance here
       return cls._instance
   def connect(self):
       # method to simulate database connection
       return "Connection established"

Ici, quelle que soit la façon dont vous l’instanciez, une seule instance partagée est utilisée, ce qui évite de dupliquer des objets identiques en mémoire.

Vérifions :

db1 = Database()
print(db1.connect())
db2 = Database()
print(db1 is db2) 

La sortie est :

Création d’instance pour le pattern singleton

Création d’instance pour le pattern Singleton

La valeur « True » indique que les variables db1 et db2 référencent la même instance.

Ces patterns montrent comment un design réfléchi peut améliorer sensiblement l’efficacité mémoire. En choisissant et en implémentant les bons patterns, vous rendez vos applications Python plus sobres et performantes.

3. Utiliser les types intégrés dans les classes

Le choix du type intégré approprié en Python peut avoir un impact majeur sur l’efficacité mémoire. Les types intégrés sont optimisés en performance et en mémoire par l’interpréteur.

Selon les cas d’usage, les tuples et les ensembles peuvent être plus économes que les listes et les dictionnaires.

Les tuples utilisent généralement moins de mémoire que les listes grâce à leur taille fixe et immuable, ce qui permet des optimisations internes. Pour des classes manipulant des données figées après création, les tuples sont un excellent choix.

Exemple pratique : classes Polygon

Voici deux classes Polygon, l’une stockant les points dans un tuple, l’autre dans une liste :

class PolygonWithTuple:
   def __init__(self, points):
       self.points = tuple(points)  # Storing points as a tuple
   def display_points(self):
       return self.points

class PolygonWithList:
   def __init__(self, points):
       self.points = list(points)  # Storing points as a list
   def display_points(self):
       return self.points

Créons des instances pour comparer la mémoire utilisée :

from pympler import asizeof

points = [(1, 2), (3, 4), (5, 6), (7, 8)]

# Creating instances of each class
polygon_tuple = PolygonWithTuple(points)
polygon_list = PolygonWithList(points)

# Memory usage comparison using pympler
print("Total memory used by Polygon with Tuple:", asizeof.asizeof(polygon_tuple))
print("Total memory used by Polygon with List:", asizeof.asizeof(polygon_list))

La sortie est :

Comparaison mémoire entre tuples et listes

Comparaison mémoire pour les tuples et les listes

Cette sortie montre que le tuple utilise généralement moins de mémoire que la liste, ce qui rend la classe PolygonWithTuple plus économe pour des données fixes comme des coordonnées.

4. Exploiter les générateurs pour l’évaluation paresseuse

En Python, les générateurs sont un atout majeur pour gérer la mémoire, surtout avec de gros volumes de données.

Ils traitent les données élément par élément, en ne chargeant en mémoire que ce qui est nécessaire à l’instant T. Cette évaluation « lazy » est idéale quand on manipule beaucoup de données mais qu’on n’en traite qu’une petite partie à la fois.

Exemple pratique : traitement de grands jeux de données

Supposons une application qui doit calculer l’âge moyen d’utilisateurs à partir d’un gros fichier. Plutôt que de charger toutes les lignes dans une liste (gourmand en mémoire), utilisons un générateur pour traiter chaque enregistrement à la volée.

def process_records(file_name):
   """Generator that yields user ages from a file one at a time."""
   with open(file_name, 'r') as file:
       for line in file:
           user_data = line.strip().split(',')
           yield int(user_data[1]) 

def average_age(file_name):
   """Calculate the average age from user records."""
   total_age = 0
   count = 0
   for age in process_records(file_name):
       total_age += age
       count += 1
   return total_age / count if count else 0

print("Average Age:", average_age('user_data.csv'))

Ici, la fonction génératrice process_records() lit le fichier ligne par ligne, extrait l’âge et le produit. Une seule ligne est donc en mémoire à un instant donné. La fonction average_age() consomme ce flux sans jamais charger l’ensemble du jeu de données.

Grâce à cette évaluation paresseuse, vous économisez de la mémoire et améliorez la réactivité et la scalabilité de vos applications.

5. Éviter les références circulaires

Les références circulaires surviennent lorsque deux objets (ou plus) se référencent mutuellement. Cela peut se produire avec des listes, des dictionnaires ou des classes où des instances se pointent les unes les autres.

Comme vu plus haut, Python dispose d’un collecteur cyclique pour détecter et nettoyer ces cycles. Mais s’en remettre à lui peut engendrer des inefficiences :

  • Nettoyage différé : il ne tourne pas en continu. Les objets en cycle peuvent donc rester en mémoire plus longtemps que nécessaire.
  • Surcoût de performance : l’identification et le nettoyage périodiques des cycles ont un coût.

Exemple pratique : structure d’arbre

Prenons une structure d’arbre où chaque nœud référence ses enfants et éventuellement son parent. Ce lien parent‑enfant forme facilement des cycles.

class TreeNode:
   def __init__(self, value, parent=None):
       self.value = value
       self.parent = parent
       self.children = []
   def add_child(self, child):
       child.parent = self  # Set parent reference
       self.children.append(child)

root = TreeNode("root")
child = TreeNode("child1", root)
root.add_child(child)

Ici, root référence child via la liste children, et child référence root via son attribut parent : un cycle potentiel.

Utiliser des références faibles pour éviter les cycles

Les références faibles (weak references) permettent de rompre/prévenir les cycles sans refondre la structure.

Le module weakref permet de référencer un objet sans augmenter son compteur de références. L’objet référencé peut donc être collecté, évitant les fuites liées aux cycles.

Voici une version modifiée utilisant des références faibles :

import weakref

class TreeNode:
   def __init__(self, value, parent=None):
       self.value = value
       self.parent = weakref.ref(parent) if parent else None
       self.children = []
   def add_child(self, child):
       child.parent = weakref.ref(self)
       self.children.append(child)
root = TreeNode("root")
child = TreeNode("child1", root)
root.add_child(child)

L’attribut parent devient une référence faible : le nœud parent peut être collecté dès qu’aucune autre référence forte ne le retient.

Cette approche prévient efficacement les fuites mémoire liées aux références circulaires.

Conclusion

Nous avons passé en revue les fondamentaux de la gestion mémoire en Python et cinq techniques avancées pour écrire des classes économes en mémoire, avec des exemples concrets et des comparaisons. Adoptez ces pratiques pour concevoir des applications plus efficaces et facilement scalables.

Nous vous invitons à découvrir notre parcours Python Programming, qui couvre les principes d’ingénierie logicielle et la programmation orientée objet, ainsi que l’écriture d’un code Python efficace.


Arunn Thevapalan's photo
Author
Arunn Thevapalan
LinkedIn
Twitter

En tant que data scientist senior, je conçois, développe et déploie des solutions d'apprentissage automatique à grande échelle pour aider les entreprises à prendre de meilleures décisions basées sur les données. En tant que rédacteur spécialisé dans la science des données, je partage mes apprentissages, mes conseils de carrière et des tutoriels pratiques approfondis.

FAQs

Que sont les __slots__ et comment aident‑ils à écrire des classes Python économes en mémoire ?

__slots__ est un mécanisme Python qui permet de déclarer explicitement les attributs d’instance et d’empêcher la création du dictionnaire d’instance. En définissant __slots__ dans une classe, Python utilise une structure bien plus économe que le dictionnaire par instance. Cela réduit la consommation mémoire, surtout lorsque l’on crée un grand nombre d’instances.

Comment les références circulaires affectent‑elles l’usage mémoire et comment les éviter ?

Les références circulaires apparaissent lorsque plusieurs objets se référencent mutuellement, créant une boucle qui empêche le ramasse-miettes de récupérer leur mémoire. Pour les éviter, utilisez des références faibles via le module weakref, repensez la conception pour supprimer des références inutiles ou cassez manuellement le cycle en mettant certaines références à None lorsqu’elles ne sont plus requises.

Quel est le rôle du ramasse‑miettes dans la gestion de la mémoire en Python ?

Le ramasse-miettes (garbage collector) complète le comptage de références en détectant et supprimant les références circulaires. Il identifie périodiquement des groupes d’objets uniquement accessibles entre eux et non utilisés ailleurs dans l’application, libérant ainsi une mémoire que le simple comptage de références ne peut pas récupérer.

Pourquoi est‑il important d’utiliser les types et structures intégrés pour l’efficacité mémoire ?

Les types et structures intégrés de Python sont fortement optimisés pour la performance et la mémoire. Utiliser ces briques (par exemple des tuples plutôt que des listes pour des données immuables, ou des sets pour des collections uniques) réduit sensiblement le surcoût mémoire par rapport à des structures inadaptées ou à des implémentations personnalisées.

Comment les outils de profilage mémoire aident‑ils à écrire des classes Python économes en mémoire ?

Des outils comme memory_profiler, pympler et tracemalloc aident à comprendre l’utilisation de la mémoire. Ils permettent d’identifier des fuites, des structures inefficaces ou des lignes de code particulièrement consommatrices, afin d’orienter précisément les optimisations.

Sujets
Science des données
Python

Approfondissez Python avec ces cours !

Cours

Introduction à Python

4 h
7.1M
Apprenez les bases de l’analyse de données avec Python en quatre heures et explorez ses principaux packages.
Voir les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow
Contenus associés

Tutoriel

Classes abstraites Python : Un guide complet avec des exemples

Découvrez les classes abstraites Python, leur utilité et comment utiliser le module `abc` pour garantir la cohérence des interfaces. Comprend des exemples pratiques et des bonnes pratiques pour une mise en œuvre efficace.
Derrick Mwiti's photo

Derrick Mwiti

10 min

Tutoriel

Cache Python : Deux méthodes simples

Apprenez à utiliser des décorateurs tels que @functools.lru_cache ou @functools.cache pour mettre en cache des fonctions en Python.
Stephen Gruppetta's photo

Stephen Gruppetta

12 min

Tutoriel

30 astuces Python pour un meilleur code, avec exemples

Nous avons sélectionné 30 astuces Python pour améliorer votre code et développer vos compétences en Python.
Kurtis Pykes 's photo

Kurtis Pykes

15 min

Tutoriel

Tutoriel Python sur les structures de données

Initiez-vous aux structures de données de Python : apprenez-en plus sur les types de données et les structures de données primitives et non primitives, telles que les chaînes de caractères, les listes, les piles, etc.
Sejal Jaiswal's photo

Sejal Jaiswal

24 min

Tutoriel

Données JSON Python : Un guide illustré d'exemples

Apprenez à utiliser JSON en Python, notamment la sérialisation, la désérialisation, le formatage, l'optimisation des performances, la gestion des API, ainsi que les limites et les alternatives de JSON.
Moez Ali's photo

Moez Ali

6 min

Tutoriel

Tutoriel et exemples sur les fonctions et méthodes des listes Python

Découvrez les fonctions et méthodes des listes Python. Veuillez suivre les exemples de code pour list() et d'autres fonctions et méthodes Python dès maintenant.
Abid Ali Awan's photo

Abid Ali Awan

7 min

Voir PlusVoir Plus