8 Formas de Implementar el Patrón de Diseño Singleton en Python

Uno de los Patrones de Diseño (Design Patterns) más sencillos y fácil de comprender es sin duda el Singleton, es decir, aquel patrón cuya finalidad es implementar una clase que permita la creación de una y solo una instancia u objeto y que provea un único punto de acceso a este. En el propio Python se emplea este patrón de diseño para implementar los objetos None, True y False. Este patrón se emplea generalmente cuando varios objetos cliente necesitan acceder a determinado objeto y queremos asegurarnos de que solo una instancia de este será creada.

Para entenderlo mejor hagamos una analogía, supongamos que tenemos la clase Plomero, que define las característica y habilidades de una persona con este oficio. Luego tenemos a Juan, que es Plomero. El Singleton en este caso se logra si siempre que solicitemos los servicios de un Plomero, nos envían irremediablemente a Juan.

La forma tradicional (en lenguajes como C++ y Java) de implementar este patrón de diseño es convirtiendo el constructor en un método privado y luego crear un método estático para inicializar el objeto, pero como Python no “permite” crear métodos privados, debemos valernos de otras herramientas para implementar el Singleton.

En Python, los pasos a seguir serían:

  1. Implementar una clase Singleton que sustituya el método __new__() (método con significado especial para Python, que se encarga de crear los objetos) por una implementación personalizada que permita la creación de un único objeto

  2. Si ya existe una instancia del objeto, proveemos el mismo objeto nuevamente

Veamos algunos ejemplos de cómo implementar esto.

1. Usando la Función Integrada hasattr()

Como sabemos Python provee la función incluida hasattr(object, name) que retorna True si el objeto tiene un atributo cuyo nombre coincida con la cadena name y False de otro modo. Valiéndonos del uso de esta función, podríamos implementar la clase Singleton de la forma siguiente:

file: singleton.py

class SingletonIfhasattr:

    def __new__(cls):

        if not hasattr(cls, 'instance'): # Si no existe el atributo “instance”

            cls.instance = super(SingletonIfhasattr, cls).__new__(cls) # lo creamos

        return cls.instance

>>> import singleton

>>> x = singleton.SingletonIfhasattr()

>>> y = singleton.SingletonIfhasattr()

>>> x is y # Los dos objetos tiene la misma identidad

True

2. Usando un try...except

Otra variante sería tratar de acceder directamente al atributo de clase instance y capturar la excepción AttributeError que se levantará si la clase no tiene el atributo requerido.

class SingletonTry:

   def __new__(cls):

        try:

            # getattr(cls, 'instance') # Primera variante

            cls.instance # Segunda variante

        except AttributeError:

            cls.instance = super(SingletonTry, cls).__new__(cls)

        return cls.instance

>>> x = singleton.SingletonTry()

>>> y = singleton.SingletonTry()

>>> x is y

True

3. Usando None

También podríamos valernos de None de la forma siguiente:

class SingletonIfNone:

    instance = None

    def __new__(cls):

        if cls.instance is None:

            cls.instance = super(SingletonIfNone, cls).__new__(cls)

        return cls.instance

>>> x = singleton.SingletonIfNone()

>>> y = singleton.SingletonIfNone()

>>> x is y

True

4. Usando class.__dict__

Como los espacios de nombres en Pyton se definen mediante el empleo de diccionarios y cada clase/objeto incluye un diccionario especial llamado __dict__ donde se guardan todos sus atributos (con sus respectivos valores), también podemos aprovechar esta característica para implementar el Singleton, que nos quedaría de la forma siguiente:

class Singleton_dict_:

    def __new__(cls):

        if not 'instance' in cls.__dict__:

            cls.instance = super(Singleton_dict_, cls).__new__(cls)

        return cls.instance

>>> x = singleton.Singleton_dict_()

>>> y = singleton.Singleton_dict_()

>>> x is y

True

En todas la implementaciones anteriores se emplea prácticamente el mismo proceder, solo cambia la forma de comprobar si existe el objeto o no, que puede ser tan variada como se nos ocurra, todo depende de cuánta imaginación podamos tener y de cuánto conozcamos las interioridades de Python. Ahora bien, sería interesante evaluar cuál de las opciones anteriores resulta más eficiente en términos de consumo de tiempo de procesamiento, pero esta tarea se la dejamos al lector.

Personalmente, prefiero la variante que emplea el bloque try...except, por el hecho de que este estilo de codificación es mucho más pythónico.

5. Empleando un Decorador

Los Decoradores se han convertido en una de las herramienta más flexibles y poderosas de Python. En este caso, también pueden ser empleados para implementar el Singleton. Lo más interesante de la propuesta que viene a continuación, es que es código totalmente reutilizable, así que lo podemos poner en nuestra colección de utilidades y emplearlo luego en cualquier situación donde necesitemos un objeto único, todo esto de forma totalmente transparente. Veamos:

class SingletonDecorator:

    def __init__(self, klass):

        self.klass = klass

        self.instance = None

    def __call__(self, *args, **kwds):

        if self.instance == None:

            self.instance = self.klass(*args, **kwds)

        return self.instance

>>> @SingletonDecorator

>>> class SomeNotSingletonClass:

… # Definimos aquí el cuerpo de la clase

>>> x = singleton.SomeNotSingletonClass()

>>> y = singleton.SomeNotSingletonClass()

>>> x is y

True

6. Empleando un Constructor Alternativo

Otra forma de implementar el Singleton es mediante lo que llamamos un Constructor Alternativo. Un Constructor Alternativo es un método de clase que podemos invocar para crear objetos a partir de determinadas condiciones iniciales. Un ejemplo clásico de este tipo de constructor en Python, es el método de clase fromkeys(seq, [value]) de la clase dict, que permite crear un diccionario a partir de una secuencia seq, cuyos elementos serán las claves del diccionario y los valores del diccionario tomarán el valor -valga la redundancia- que le asignemos al parámetro value que por defecto es None. Veamos cómo nos podría quedar el Singleton si lo implementamos a través de un Constructor Alternativo.

class Singleton:

    _instance = None

    @classmethod

    def get_instance(cls): # Constructor alternativo que retorna una nueva instancia

        if not cls._instance:

            cls._instance = cls()

        return cls._instance

>>> x = singleton.Singleton.get_instance()

>>> y = singleton.Singleton.get_instance()

>>> x is y

True

Este método tiene de interesante que esta puede ser una clase cualquiera que podemos usar de forma tradicional o de forma Singleton si lo deseamos o necesitamos en determinado momento. Es decir, tendríamos una clase mucho más flexible que no está condenada a ser Singleton obligatoriamente.

7. El Borg de Alex Martelli

Según Alex Martelli, desarrollador y autor destacado de la Comunidad de Python, con frecuencia los programadores necesitamos crear varios objetos de la misma clase que compartan un mismo estado, es decir, que comparta el mismo conjunto de valores en sus atributos, sin compartir una misma identidad. Esto es posible lograrlo con una variante del Singleton denominada Borg y propuesta por el propio Martelli.

class Borg:

    _shared_state = {}

    def __init__(self):

        self.__dict__ = self._shared_state

class Singleton(Borg):

    def __init__(self, arg):

        Borg.__init__(self)

        self.val = arg

    def __str__(self):

        return self.val

>>> x = singleton.Singleton(4)

>>> x.val

4

>>> y = singleton.Singleton(8)

>>> y.val

8

>>> x.val

8

>>> x is y

False

Como vemos, hemos logrado que el atributo val sea común a ambos objetos, sin tener un objeto único.

8. Emular el Singleton con una Variable de Nivel Módulo

En el mundillo de Python muchos piensan que no es necesario emplear el patrón Singleton, pues siempre podemos emularlo empleando una variable global a nivel de módulo que provea el objeto que necesitamos. En Python, este tipo de variables suelen llamarse “constantes” (una mejor forma de llamarlas es: variables que nunca cambian) y se escriben, según las buenas prácticas de estilo definidas en el PEP 8, en mayúsculas sostenidas. Esta forma de emular el Singleton tiene la desventaja de que el objeto se crea en tiempo de importación y una vez creado estará disponible durante toda la ejecución de la aplicación, lo necesitemos o no.

file: mymodule.py

SINGLETON = SomeNotSingletonClass()

>>> import mymodule

>>> x = mymodule.SINGLETON

>>> y = mymodule.SINGLETON

>>> x is y # Obviamente arrojará un valor verdadero

True

Lecturas Recomendadas

Para profundizar en el estudio de estos temas recomendamos la lectura del Capítulo 2. “The Singleton Design Pattern” del libro “Learning Python Design Patterns.”, Segunda Edición por Chetan Giridhar, publicado por la Editorial “Pakt Publishing”.

Muy bien, esto es todo por ahora, si este artículo te resultó interesante y/o útil, compártelo para que otros también puedan acceder a él. Déjanos tus comentarios y podremos mejorar nuestros contenidos.

Sin se te ocurre otra forma en que podamos implementar el Singleton, déjala en un comentario.

Gracias de antemano,

lpozo

2 comentarios

    • Cesar en 11 abril, 2018 a las 3:09 pm

    Hola, comentar que Singelton también esta considerado como un anti-patrón (anti-pattern) debido a que la mayoría de las veces hace (entre otras cosas) que el código sea difícil de reutilizar y de probar debido a que queda estrechamente acoplado al Singelton. Esto no significa que no haya ocasiones en el que sea necesario utilizarlo, por ejemplo, cuando necesitamos trabajar con una única conexión a una BD. Pero hay que ser muy cuidadoso a la hora de usarlo y tener en cuanta que la mayoría de las veces existen formas mejores de resolver los problemas.

    Saludos.

    1. Es válida tu aclaración Cesar, el Singleton tiene muchos detractores, pero aún se emplea en muchísimas situaciones. Python le pone fexibilidad añadida a este patrón, tal es el caso de las variantes 5 y 6, donde no estás obligado ha definir una clase como Singleton desde su nacimiento y para siempre. En fin, cuando hay que usarlo, hay que usarlo.
      saludos,
      lpozo

Los comentarios han sido desactivados.