Python >> Tutoriel Python >  >> Python

Comment créer un singleton en Python ?

Les maîtres codeurs se comportent comme des architectes qui se connectent et s'appuient sur divers modèles de conception pour créer un ensemble fonctionnel. L'un des modèles de conception les plus importants est un singleton — une classe qui n'a qu'une seule instance. Vous pouvez demander :À quoi cela ressemble-t-il ? Examinons le code implémentant un singleton dans notre shell de code interactif :

Exercice :essayez de créer plusieurs instances de la classe singleton. Peux-tu le faire ?

Plongeons dans une compréhension plus profonde du singleton. Nous discuterons de ce code dans notre première méthode, alors continuez à lire !

Qu'est-ce qu'un singleton ?

Un singleton est une classe qui n'a qu'une seule instance. Toutes les variables de la classe pointent vers la même instance. Il est simple et direct à créer et à utiliser et c'est l'un des modèles de conception décrits par le Gang of Four. Après avoir créé la première instance, toutes les autres créations pointent vers la première instance créée. Il résout également le problème d'avoir un accès global à une ressource sans utiliser de variables globales. J'aime cette définition concise de Head First Design Patterns :

Pourquoi auriez-vous besoin d'un singleton ?

Si vous lisez ceci, vous avez probablement déjà une utilisation possible. Singleton est l'un des Créationnels du Gang des Quatre motifs. Lisez la suite pour déterminer si c'est un bon candidat pour le problème que vous devez résoudre.

Un singleton peut être utilisé pour accéder à une ressource commune comme une base de données ou un fichier. Il y a une petite controverse sur son utilisation. En fait, la controverse pourrait être décrite comme une honte pure et simple. Si cela vous concerne, j'ai énuméré certaines des objections ci-dessous avec quelques liens. Malgré tout cela, les singletons peuvent être utiles et pythoniques. Extrait de Le Zen de Python (Les pythonistes disent Ohm):

  • Simple vaut mieux que complexe
  • L'aspect pratique l'emporte sur la pureté

Néanmoins, les objections ont du mérite et peuvent s'appliquer au code sur lequel vous travaillez actuellement. Et même si elles ne s'appliquent pas, la compréhension de ces objections peut vous permettre de mieux comprendre les principes orientés objet et les tests unitaires.

Un singleton peut être utile pour contrôler l'accès à tout ce qui change globalement lorsqu'il est utilisé. En plus des bases de données et des fichiers, un singleton peut fournir des avantages pour l'accès à ces ressources :

  • Enregistreur
  • Pools de threads
  • caches
  • boîtes de dialogue
  • Un client HTTP
  • gère ​​les paramètres de préférence
  • objets pour la journalisation
  • gère ​​les pilotes de périphériques tels que les imprimantes.
  • (?) Toute ressource unique ou collection globale

Un singleton peut être utilisé à la place d'une variable globale. Les variables globales sont potentiellement désordonnées. Les singletons présentent certains avantages par rapport aux variables globales. Un singleton peut être créé avec une création impatiente ou paresseuse. La création avide peut créer la ressource au démarrage du programme. La création différée créera l'instance uniquement lorsqu'elle sera nécessaire pour la première fois. Les variables globales utiliseront une création impatiente, que cela vous plaise ou non. Les singletons ne polluent pas l'espace de noms global.

Et enfin, un singleton peut faire partie d'un modèle de conception plus large. Il peut faire partie de l'un des modèles suivants :

  • motif d'usine abstrait
  • modèle de constructeur
  • modèle prototype
  • motif de façade
  • modèle d'objets d'état Si vous n'en avez pas entendu parler, pas de soucis. Cela n'affectera pas votre compréhension du modèle singleton.

Mise en œuvre

Les implémentations standard C# et Java reposent sur la création d'une classe avec un constructeur privé. L'accès à l'objet est donné par une méthode :getInstance()

Voici une implémentation typique de singleton paresseux en Java :
public Singleton {
    private static Singleton theOnlyInstance;
    private Singleton() {}                   
    public static Singleton getInstance() {  
        if (theOnlyInstance) == null){
            theOnlyInstance = new Singleton()
        }
            return new Singleton();
    }
}

Il existe de nombreuses façons d'implémenter Singleton en Python . Je vais d'abord montrer les quatre et en discuter ci-dessous.

Méthode 1 :Utiliser __new__

class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(Singleton, cls).__new__(cls)
            # Initialize here. No need to use __init__()..
            cls.val = 0
        return cls._instance
    
    def business_method(self, val):
        self.val = val

x = Singleton()
y = Singleton()
x.val = 42
x is y, y.val

Il utilise le Python dunder __new__ qui a été ajouté à Python pour fournir une méthode alternative de création d'objet. C'est le genre de cas d'utilisation __new__ a été conçu pour

Avantages :

  • Je pense que cette mise en œuvre est la plus proche dans l'esprit de la mise en œuvre du GoF. Il semblera familier à toute personne familiarisée avec l'implémentation standard de Singleton.
    • La signification du code facile à comprendre est importante pour les équipes et la maintenance.
  • Utilise une classe pour créer et implémenter le Singleton.

Inconvénients :

  • Malgré sa "correction", de nombreux codeurs Python devront rechercher __new__ pour comprendre les spécificités de la création d'objets. C'est assez de savoir que
    1. __new__ instancie l'objet.
    2. Code qui va normalement dans __init__ peut être placé en __new__ .
    3. Afin de fonctionner correctement, le __new__ remplacé doit appeler le __new__ de son parent méthode. Dans ce cas, l'objet est le parent. L'instanciation se produit ici avec cette ligne :
      • object.__new__(class_, *args, **kwargs)

Méthode 2 :Un décorateur

def singleton(Cls):
    singletons = {}
    def getinstance(*args, **kwargs):
        if Cls not in singletons:
            singletons[Cls] = Cls(*args, **kwargs)
        return singletons[Cls]
    
    return getinstance

@singleton
class MyClass:
    def __init__(self):
        self.val = 3

x = MyClass()
y = MyClass()
x.val = 42
x is y, y.val, type(MyClass)

Avantages

  • Le code pour écrire le décorateur est séparé de la création de la classe.
  • Il peut être réutilisé pour créer autant de singletons que nécessaire.
  • Le décorateur singleton marque une intention claire et compréhensible

Inconvénients

  • L'appel type(MyClass) se résoudra en tant que fonction .
    • Création d'une méthode de classe en MyClass entraînera une erreur de syntaxe.

Si vous voulez vraiment utiliser un décorateur et que vous devez conserver la définition de classe, il existe un moyen. Vous pouvez utiliser cette bibliothèque :

pip install singleton_decorator

La bibliothèque singleton_decorator enveloppe et renomme la classe singleton. Alternativement, vous pouvez écrire le vôtre. Voici une implémentation :

def singleton(Cls):
    class Decorated(Cls):
        
        def __init__(self, *args, **kwargs):
            if hasattr(Cls, '__init__'):
                Cls.__init__(self, *args, **kwargs)
                
        def __repr__(self) : 
            return Cls.__name__ + " obj"
        
        __str__ = __repr__
        
    Decorated.__name__ = Cls.__name__
    
    class ClassObject:
        
        def __init__(cls):
            cls.instance = None
            
        def __repr__(cls):
            return Cls.__name__
        
        __str__ = __repr__
        
        def __call__(cls, *args, **kwargs):
            if not cls.instance:
                cls.instance = Decorated(*args, **kwargs)
            return cls.instance
    return ClassObject()

@singleton
class MyClass():
    pass

x = MyClass()
y = MyClass()
x.val = 42
x is y, y.val

La sortie est :

(True, 42)

Exercice interactif :Exécutez la visualisation de mémoire interactive suivante. Combien d'instances singleton trouvez-vous ?

Méthode 3 :Utiliser la métaclasse et hériter du type et remplacez __call__ pour déclencher ou filtrer la création d'une instance

class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class MyClass(metaclass=Singleton):
    pass

x = MyClass()
y = MyClass()
x.val=4
x is y, y.val

Le résultat est le suivant :

(True, 4)

La méthode 3 crée une nouvelle métaclasse personnalisée en héritant de type. MyClass attribue alors Singleton comme métadonnée :

class MyClass(metadata = Singleton):

Les mécaniques de la classe Singleton sont intéressantes. Il crée un dictionnaire pour contenir les objets singleton instanciés. Les clés dict sont les noms de classe. Dans le __call__ remplacé méthode, super.__call__ est appelée pour créer l'instance de classe. Voir la métaclasse personnalisée pour mieux comprendre le __call__ méthode.

Avantages

  • Le code singleton est séparé. Plusieurs singletons peuvent être créés en utilisant le même

Inconvénients

  • Les métaclasses restent mystérieuses pour de nombreux codeurs Python. Voici ce que vous devez savoir :
    • Dans cette implémentation, le type est hérité :
      • class Singleton(type)
    • Afin de fonctionner correctement, le __call__ remplacé doit appeler le __call__ de son parent méthode.
      • cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)

Méthode 4 :Utiliser une classe de base

class Singleton:
    _instance = None
    def __new__(class_, *args, **kwargs):
        if not isinstance(class_._instance, class_):
            class_._instance = object.__new__(class_, *args, **kwargs)
        return class_._instance

class MyClass(Singleton):
    pass
x = MyClass()
y = MyClass()
x.val=4
x is y, y.val

Le résultat est le suivant :

(True, 4)

Avantages

  • Le code peut être réutilisé pour créer plus de singletons
  • Utilise des outils familiers. (Par rapport aux décorateurs, aux métaclasses et au __new__ méthode)

Dans les quatre méthodes, une instance est créée la première fois qu'on lui en demande une. Tous les appels après le premier renvoient la première instance.

Singletons dans un environnement de threads

Si votre Singleton doit fonctionner dans un environnement multithread, votre méthode Singleton doit être sécurisée pour les threads. Aucune des méthodes ci-dessus n'est thread-safe. Le code vulnérable est trouvé entre la vérification d'un Singleton existant et la création de la première instance :

if cls._instance is None:
    cls._instance = super(Singleton, cls).__new__(cls)

Chaque implémentation a un morceau de code similaire. Pour le rendre thread-safe, ce code doit être synchronisé.

with threading.Lock():
    if cls._instance is None:
        cls._instance = super(Singleton, cls).__new__(cls)


Cela fonctionne bien et avec le verrou en place, la création Singleton devient thread-safe. Maintenant, chaque fois qu'un thread exécute le code, le threading.Lock() est appelée avant de rechercher une instance existante.

Si les performances ne sont pas un problème, c'est très bien, mais nous pouvons faire mieux. Le mécanisme de verrouillage est coûteux et il ne doit fonctionner que la première fois. La création d'instance ne se produit qu'une seule fois, le verrouillage ne doit donc se produire qu'une seule fois. La solution consiste à placer le verrou après l'instruction de vérification. Ajoutez ensuite une autre coche après le verrou.

import threading
...
    if cls._instance is None:
        with threading.Lock():
            if cls._instance is None: 
                cls._instance = super(Singleton, cls).__new__(cls)

Et c'est ainsi qu'il faut utiliser le "Verrouillage à double contrôle".

Version thread-safe de la méthode 1

Considérez la modification suivante de la méthode 1 :

import threading
class Singleton:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            with threading.Lock():
                if cls._instance is None:
                    cls._instance = super(Singleton, cls).__new__(cls)
                    # Initialize here. No need to use __init__()..
                    cls.val = 0
        return cls._instance
    
    def business_method(self, val):
        self.val = val

x = Singleton()
y = Singleton()
x.val = 42
x is y, y.val

La sortie est :

(True, 42)

Pour le rendre thread-safe, nous avons ajouté deux lignes de code. Chaque méthode pourrait être rendue thread-safe de la même manière

Alternatives à l'utilisation d'un Singleton

Utiliser un module comme singleton (The Global Object Pattern)

En Python, les modules sont uniques, uniques et disponibles dans le monde entier. Le modèle d'objet global est recommandé par la documentation Python. Cela signifie simplement créer un module séparé et instancier votre objet dans l'espace global du module. Les références suivantes n'ont qu'à l'importer.

Utiliser l'injection de dépendance

Généralement, cela signifie utiliser la composition pour fournir des objets aux objets dépendants. Il peut être implémenté d'innombrables façons, mais généralement, placez des dépendances dans les constructeurs et évitez de créer de nouvelles instances d'objets dans les méthodes métier.

Les problèmes avec les singletons

Parmi les 23 modèles du livre phare Design Patterns de 1994, Singleton est le plus utilisé, le plus discuté et le plus critiqué. C'est un peu un trou de lapin pour passer au crible les milliers de blogs et de messages Stack Overflow qui en parlent. Mais après toute la haine de Singleton, le modèle reste courant. Pourquoi donc? C'est parce que les conditions qui suggèrent son utilisation sont très courantes :Une base de données, un fichier de configuration, un pool de threads…

Les arguments contre son utilisation sont mieux exposés dans certains articles de blog élégants (et anciens) que je ne peux pas égaler. Mais je vais donner un résumé et des liens pour une lecture plus approfondie.

Résumé concis

Paraphrasé de Brian Button dans Why Singletons are Evil :

  1. Ils sont généralement utilisés comme une instance globale, pourquoi est-ce si mauvais ? Parce que vous cachez les dépendances de votre application dans votre code, au lieu de les exposer via les interfaces. Faire quelque chose de global pour éviter de le faire circuler est une odeur de code. (Ce sont des injures efficaces. Quelle que soit l'odeur du code, cela me fait grincer des dents un peu et plisser le nez comme je l'imagine).
  2. Ils violent le principe de responsabilité unique :en vertu du fait qu'ils contrôlent leur propre création et cycle de vie.
  3. Ils provoquent intrinsèquement un couplage étroit du code. Cela rend les simulations sous test plutôt difficiles dans de nombreux cas.
  4. Ils conservent l'état pendant toute la durée de vie de l'application. Un autre coup dur pour les tests puisque vous pouvez vous retrouver avec une situation où les tests doivent être commandés, ce qui est un grand non non pour les tests unitaires. Pourquoi? Parce que chaque test unitaire doit être indépendant des autres.

Devez-vous utiliser des singletons dans votre code ?

Si vous vous posez la question en vous basant sur les blogs des autres, vous êtes déjà dans le terrier du lapin. Le mot "devrait" n'est pas le bienvenu dans la conception du code. Utilisez ou non des singletons et soyez conscient des problèmes éventuels. Refactoriser en cas de problème.

Problèmes possibles à prendre en compte

Les outils sont destinés aux personnes qui savent les utiliser. Malgré toutes les mauvaises choses écrites sur les Singletons, les gens les utilisent toujours parce que :

  1. Ils répondent mieux à un besoin que les alternatives.

et/ou

  1. Ils ne savent pas mieux et ils créent des problèmes dans leur code en les utilisant.

Évitez les problèmes. Ne soyez pas dans le groupe 2.

Les problèmes avec les singletons sont dus au fait qu'ils enfreignent la règle de la responsabilité unique. Ils font trois choses :

  1. Garantir qu'une seule instance existe
  2. Fournir un accès global à cette instance
  3. Fournir leur propre logique métier
  • Parce qu'ils enfreignent la règle de la responsabilité unique, les singletons peuvent être difficiles à tester
    • L'inversion de l'IoC de contrôle et l'injection de dépendances sont des modèles destinés à résoudre ce problème d'une manière orientée objet qui aide à créer un code testable.
  • Les singletons peuvent entraîner un code étroitement couplé. Une instance globale qui a un état inconstant peut exiger qu'un objet dépende de l'état de l'objet global.
  • Il s'agit d'un principal OO pour séparer la logique de création de la logique métier. Adhérant à ce principe "Les singletons devraient ne jamais être utilisé ». Encore une fois avec le mot devrait. Au lieu de cela, soyez Yoda :"Fais ou ne fais pas ! “. Basez la décision sur votre propre code.
  • La mémoire allouée à un singleton ne peut pas être libérée. Ce n'est un problème que si la mémoire doit être libérée.
    • Dans un environnement de récupération de place, les singletons peuvent devenir un problème de gestion de la mémoire.

Étude complémentaire

  • Brandon Rhodes, Le modèle Singleton
  • Miško Hevery, singleton I Love You-But You're Bringing Me Down. Republié avec des commentaires
  • Miško Hevery, les célibataires sont des menteurs pathologiques
  • Miško Hevery, où sont passés tous les célibataires
  • Wikipédia Singleton_pattern
  • Michael Safayan, Anti-Pattern Singleton
  • Mark Radford Singleton, l'anti-modèle
  • Alex Miller, Patterns I Hate #1 :Singleton
  • Scott Densmore/Brian Button, Pourquoi les célibataires sont mauvais
    • Martin Brampton, Les singletons bien utilisés sont BON !
  • Une discussion éditée par Cunningham &Cunningham, Singleton Global Problems
  • Robert Nystrom, Modèles de conception revisités :Singleton
  • Steve Yegge, Singleton considéré comme stupide
  • J. B. Rainsberger Utilisez vos singletons à bon escient

Meta notes — Miško Hevery.

Hevery travaillait chez Google lorsqu'il a écrit ces blogs. Ses blogs étaient lisibles, divertissants, informatifs, provocateurs et généralement exagérés pour faire valoir un point. Si vous lisez ses blogs, assurez-vous de lire les commentaires. Les singletons sont des menteurs pathologiques a un exemple de test unitaire qui illustre comment les singletons peuvent rendre difficile la compréhension des chaînes de dépendance et le démarrage ou le test d'une application. C'est un exemple assez extrême d'abus, mais il fait valoir un argument valable :

Bien sûr, il exagère un peu. Les singletons enveloppent l'état global dans une classe et sont utilisés pour des choses qui sont "naturellement" globales par nature. Généralement, Hevery recommande l'injection de dépendances pour remplacer les Singletons. Cela signifie simplement que les objets reçoivent leurs dépendances dans leur constructeur.

Où sont passés tous les singletons, cela montre que l'injection de dépendances a facilité l'obtention d'instances pour les constructeurs qui en ont besoin, ce qui atténue le besoin sous-jacent derrière les mauvais singletons globaux décriés dans les menteurs pathologiques.

Meta notes — Brandon Rhodes The Singleton Pattern

Meta notes — J.B. Rainsberger Use your singletons sagement

Sachez quand utiliser des singletons et quand les laisser derrière

J.B. Rainsberger

Publié le 1er juillet 2001 Les tests unitaires automatisés sont plus efficaces lorsque :

  • Le couplage entre les classes est seulement aussi fort qu'il doit l'être
  • Il est simple d'utiliser des implémentations fictives de classes collaboratives à la place des implémentations de production
Les célibataires en savent trop

Il y a un anti-modèle d'implémentation qui s'épanouit dans une application avec trop de singletons :l'anti-modèle Je sais où tu habites. Cela se produit lorsque, parmi les classes qui collaborent, une classe sait où obtenir les instances de l'autre.

Vers des singletons acceptables

L'abus de singleton peut être évité en regardant le problème sous un angle différent. Supposons qu'une application n'ait besoin que d'une seule instance d'une classe et que l'application configure cette classe au démarrage :pourquoi la classe elle-même devrait-elle être responsable d'être un singleton ? Il semble assez logique que l'application assume cette responsabilité, puisque l'application nécessite ce genre de comportement. L'application, et non le composant, doit être le singleton. L'application rend ensuite une instance du composant disponible pour tout code spécifique à l'application à utiliser. Lorsqu'une application utilise plusieurs de ces composants, elle peut les agréger dans ce que nous avons appelé une boîte à outils.

Meta notes — Mark Safayan Singleton anti pattern