Qu'est-ce que le pattern Singleton en Python ?

Découvrez le pattern Singleton en Python : définition, implémentations avec __new__, décorateur et métaclasse, bonnes pratiques et cas d'usage concrets.
10 min de lecture
Believemy logo

Définition du Singleton en Python

Le Singleton est un design pattern (patron de conception) qui garantit qu'une class ne possède qu'une seule et unique instance dans l'ensemble de votre programme. Chaque appel ultérieur au constructeur retourne toujours la même instance, plutôt que d'en créer une nouvelle.

Ce pattern fait partie des patrons de création (creational patterns) décrits dans le célèbre ouvrage Design Patterns: Elements of Reusable Object-Oriented Software du Gang of Four. En Python, il existe plusieurs façons élégantes de l'implémenter, que nous allons explorer ensemble dans cet article. Si vous souhaitez approfondir vos connaissances en programmation orientée objet, n'hésitez pas à suivre notre formation complète sur Python.

Bon à savoir

Le Singleton est l'un des design patterns les plus connus, mais aussi l'un des plus débattus. Il est essentiel de comprendre quand l'utiliser — et quand l'éviter.


Pourquoi utiliser un Singleton ?

Le Singleton répond à un besoin précis : contrôler l'accès à une ressource partagée unique. Voici les cas d'usage les plus courants :

  • Connexion à une base de données : vous ne souhaitez pas ouvrir plusieurs connexions inutiles.
  • Fichier de configuration : un seul objet centralise la lecture de la configuration de votre application.
  • Logger (journal de logs) : un unique point d'entrée pour écrire les messages de log.
  • Cache applicatif : un dict partagé qui stocke des données temporaires.
  • Pool de threads ou de connexions : un gestionnaire unique qui distribue les ressources.

L'idée fondamentale est que certains objets n'ont de sens que s'ils existent en un seul exemplaire. Créer plusieurs instances de ces objets pourrait entraîner des incohérences, des conflits d'accès ou un gaspillage de ressources.


Implémentation avec __new__

La méthode la plus classique pour implémenter un Singleton en Python consiste à surcharger la méthode __new__ de votre classe. Cette méthode spéciale est appelée avant __init__ et contrôle la création de l'instance.

PYTHON
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, valeur=None):
        # Attention : __init__ est appelé à chaque appel de Singleton()
        if valeur is not None:
            self.valeur = valeur


# Utilisation
a = Singleton("premier")
b = Singleton("second")

print(a is b)       # True
print(a.valeur)     # second
print(id(a) == id(b))  # True

Analysons ce qui se passe étape par étape :

  1. Lors du premier appel Singleton("premier"), _instance est None, donc __new__ crée une nouvelle instance.
  2. Ensuite, __init__ est appelé et affecte self.valeur = "premier".
  3. Lors du second appel Singleton("second"), _instance existe déjà, donc __new__ retourne la même instance.
  4. __init__ est à nouveau appelé, écrasant self.valeur avec "second".
Attention

Attention : avec cette implémentation, __init__ est appelé à chaque instanciation. Si vous y placez de la logique d'initialisation, elle sera exécutée plusieurs fois. Utilisez un flag pour contrôler cela.


Version protégée avec flag d'initialisation

Pour éviter que __init__ ne soit exécuté plusieurs fois, vous pouvez ajouter un attribut de contrôle :

PYTHON
class Singleton:
    _instance = None
    _initialized = False

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, valeur=None):
        if not self._initialized:
            self.valeur = valeur
            self._initialized = True


a = Singleton("premier")
b = Singleton("second")

print(a.valeur)  # premier
print(b.valeur)  # premier (même instance, non réinitialisée)

Cette fois, la valeur "premier" est conservée car __init__ ne s'exécute qu'une seule fois.


Implémentation avec un décorateur

Une approche plus pythonique consiste à utiliser un décorateur. Ce décorateur encapsule n'importe quelle classe et la transforme en Singleton, sans modifier son code interne :

PYTHON
def singleton(cls):
    instances = {}

    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]

    return get_instance


@singleton
class Configuration:
    def __init__(self, fichier="config.yml"):
        self.fichier = fichier
        self.parametres = self._charger()

    def _charger(self):
        # Simulation du chargement
        return {"debug": True, "version": "1.0"}


# Utilisation
config1 = Configuration()
config2 = Configuration("autre_config.yml")

print(config1 is config2)        # True
print(config1.fichier)           # config.yml
print(config2.parametres)        # {'debug': True, 'version': '1.0'}

Cette solution est élégante car elle sépare la logique du Singleton de la logique métier de la classe. Le décorateur stocke les instances dans un dict local, ce qui permet même de gérer plusieurs classes Singleton indépendantes.


Implémentation avec une métaclasse

Pour les cas les plus avancés, vous pouvez utiliser une métaclasse. Une métaclasse contrôle la création des classes elles-mêmes, ce qui en fait un outil puissant pour le pattern Singleton :

PYTHON
class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
        return cls._instances[cls]


class Logger(metaclass=SingletonMeta):
    def __init__(self):
        self.messages = []

    def log(self, message):
        self.messages.append(message)
        print(f"[LOG] {message}")


# Utilisation
logger1 = Logger()
logger1.log("Démarrage de l'application")

logger2 = Logger()
logger2.log("Connexion utilisateur")

print(logger1 is logger2)           # True
print(len(logger1.messages))        # 2
print(logger1.messages)             # ['Démarrage de l\'application', 'Connexion utilisateur']

La métaclasse SingletonMeta intercepte l'appel __call__ (déclenché quand vous écrivez Logger()) et vérifie si une instance existe déjà. Cette approche est la plus robuste et fonctionne parfaitement avec l'héritage.


Implémentation avec un module Python

Il existe une approche encore plus simple, propre à Python : utiliser un module comme Singleton. En Python, les modules sont des objets à instance unique par nature. Quand vous importez un module, Python le charge une seule fois et le met en cache.

PYTHON
# fichier: mon_singleton.py

class _Configuration:
    def __init__(self):
        self.debug = True
        self.version = "1.0"
        self.base_url = "https://example.com"

# Instance unique au niveau du module
config = _Configuration()

def get_parametre(cle):
    return getattr(config, cle, None)
PYTHON
# fichier: main.py
from mon_singleton import config, get_parametre

print(config.debug)              # True
print(get_parametre("version"))  # 1.0

# Partout dans votre code, config est la même instance
config.debug = False

Cette approche est souvent recommandée par la communauté Python car elle est simple, lisible et idiomatique. Pas besoin de class complexe ni de métaclasse.


Comparaison des différentes approches

MéthodeComplexitéHéritageThread-safePythonique
__new__Moyenne⚠️ Partiel❌ Non✅ Oui
DécorateurFaible❌ Non❌ Non✅ Oui
MétaclasseÉlevée✅ Oui❌ Non⚠️ Avancé
ModuleTrès faibleN/A✅ Oui*✅✅ Très

* Le mécanisme d'import de Python est thread-safe par défaut grâce au GIL.


Singleton thread-safe

Si votre application est multi-thread, les implémentations classiques du Singleton ne sont pas sûres. Deux threads pourraient passer la vérification if _instance is None simultanément. Voici une version thread-safe avec un verrou :

PYTHON
import threading


class SingletonThreadSafe:
    _instance = None
    _lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            with cls._lock:
                # Double vérification (double-checked locking)
                if cls._instance is None:
                    cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        pass


# Test avec plusieurs threads
def creer_instance():
    instance = SingletonThreadSafe()
    print(f"Thread {threading.current_thread().name}: {id(instance)}")


threads = [threading.Thread(target=creer_instance) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

Le double-checked locking est une technique d'optimisation : le premier if évite d'acquérir le verrou inutilement si l'instance existe déjà, tandis que le second if (à l'intérieur du with) assure la sécurité thread.


Exemple concret : gestionnaire de base de données

Voyons un exemple réaliste d'utilisation du Singleton pour gérer une connexion à une base de données :

PYTHON
import sqlite3
import threading


class DatabaseManager(metaclass=SingletonMeta):
    def __init__(self, db_path="app.db"):
        self.db_path = db_path
        self._connection = None
        self._lock = threading.Lock()

    @property
    def connection(self):
        if self._connection is None:
            self._connection = sqlite3.connect(self.db_path)
        return self._connection

    def executer(self, requete, params=None):
        with self._lock:
            curseur = self.connection.cursor()
            if params:
                curseur.execute(requete, params)
            else:
                curseur.execute(requete)
            self.connection.commit()
            return curseur

    def recuperer(self, requete, params=None):
        curseur = self.executer(requete, params)
        return curseur.fetchall()

    def fermer(self):
        if self._connection:
            self._connection.close()
            self._connection = None


# Utilisation partout dans l'application
db = DatabaseManager("mon_application.db")
db.executer("CREATE TABLE IF NOT EXISTS utilisateurs (id INTEGER PRIMARY KEY, nom TEXT)")
db.executer("INSERT INTO utilisateurs (nom) VALUES (?)", ("Alice",))

# Ailleurs dans le code, même instance
db2 = DatabaseManager()
resultats = db2.recuperer("SELECT * FROM utilisateurs")
print(resultats)  # [(1, 'Alice')]

Ici, peu importe où vous instanciez DatabaseManager() dans votre code, vous obtenez toujours la même connexion. Cela évite d'ouvrir plusieurs connexions inutiles et centralise la gestion de la base de données.


Bonnes pratiques et mises en garde

Quand utiliser le Singleton

  • Vous avez besoin d'un point d'accès global unique à une ressource.
  • La création de l'objet est coûteuse (connexion réseau, lecture de fichier).
  • L'objet doit maintenir un état cohérent partagé entre plusieurs parties du code.


Quand éviter le Singleton

  • Tests unitaires : le Singleton introduit un état global qui rend les tests difficiles à isoler. Préférez l'injection de dépendances.
  • Couplage fort : les classes qui dépendent d'un Singleton sont couplées à son implémentation.
  • Violation du principe de responsabilité unique : le Singleton gère à la fois sa logique métier et son cycle de vie.
  • Pas besoin d'état : si votre classe n'a pas d'état, utilisez simplement des fonctions au niveau du module.
Attention

Le Singleton est parfois qualifié d'anti-pattern dans la communauté Python. Avant de l'utiliser, demandez-vous si un simple module ou l'injection de dépendances ne serait pas plus adapté.


Conseils pour une bonne implémentation

  1. Préférez l'approche module pour les cas simples — c'est la manière la plus pythonique.
  2. Utilisez la métaclasse si vous avez besoin d'héritage ou d'une structure orientée objet stricte.
  3. Protégez les accès concurrents avec un verrou (threading.Lock) en environnement multi-thread.
  4. Documentez clairement que votre classe est un Singleton — ajoutez une docstring explicite.
  5. Prévoyez une méthode de réinitialisation pour faciliter les tests unitaires.
PYTHON
class Singleton:
    """Classe Singleton — une seule instance est créée.
    
    Utilisez Singleton.reset() dans les tests pour réinitialiser l'instance.
    """
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    @classmethod
    def reset(cls):
        """Réinitialise l'instance (utile pour les tests)."""
        cls._instance = None


Questions fréquentes

Question

Quelle est la différence entre un Singleton et une variable globale ?

Une variable global est simplement une valeur accessible partout, sans aucun contrôle sur sa création ou sa modification. Le Singleton, lui, encapsule la logique de création et garantit qu'une seule instance existe. Il offre un contrôle strict via la classe elle-même, tandis qu'une variable globale peut être écrasée à tout moment.


Question

Le Singleton fonctionne-t-il avec l'héritage en Python ?

Cela dépend de l'implémentation. Avec la méthode __new__, l'héritage peut poser problème car l'attribut _instance est partagé. Avec une métaclasse, chaque sous-classe aura sa propre entrée dans le dictionnaire _instances, ce qui permet un Singleton par classe dans la hiérarchie d'héritage.


Question

Le Singleton est-il thread-safe en Python ?

Non, les implémentations classiques ne sont pas thread-safe. Même si le GIL (Global Interpreter Lock) offre une certaine protection, il ne garantit pas l'atomicité de la vérification et de la création. Pour un Singleton thread-safe, utilisez un threading.Lock avec le pattern de double-checked locking, ou optez pour l'approche par module qui est naturellement thread-safe.


Question

Comment apprendre à maîtriser les design patterns en Python ?

Les design patterns comme le Singleton deviennent naturels avec la pratique et une solide compréhension de la programmation orientée objet en Python. Nous vous recommandons de suivre notre formation Python sur Believemy, qui couvre en profondeur les classes, les métaclasses et les bonnes pratiques de conception logicielle. Vous y apprendrez à choisir le bon pattern pour chaque situation concrète.

Termes connexes

Découvrez notre glossaire Python

Parcourez les termes et définitions les plus couramment utilisés dans le domaine du développement avec Python.

Partager cet article

Tu veux nous aider ? Fais un lien vers cet article sur tes réseaux ou encore mieux : sur ton site, dans un article ou dans ta newsletter.