Patrón Singleton en Python: una guía completa

Un patrón Singleton en python es un patrón de diseño que le permite crear solo una instancia de una clase, a lo largo de la vida útil de un programa. El uso de un patrón singleton tiene muchos beneficios. Algunos de ellos son:

  • Para limitar el acceso simultáneo a un recurso compartido.
  • Para crear un punto de acceso global para un recurso.
  • Para crear solo una instancia de una clase, a lo largo de la vida útil de un programa.

Diferentes formas de implementar un Singleton:

Un patrón singleton se puede implementar de tres maneras diferentes. Son los siguientes:

  • Singleton a nivel de módulo
  • Singleton clásico
  • Borg Singleton

Singleton a nivel de módulo:

Todos los módulos son singleton, por definición. Vamos a crear un singleton simple a nivel de módulo donde los datos se comparten entre otros módulos. Aquí crearemos tres archivos python: singleton.py, sample_module1.py y sample_module2.py, en los que los otros módulos de muestra comparten una variable de singleton.py. 

## singleton.py
shared_variable = "Shared Variable"

 singleton.py

## samplemodule1.py
import singleton
print(singleton.shared_variable)
singleton.shared_variable += "(modified by samplemodule1)"

módulo de muestra1.py

##samplemodule2.py
import singleton
print(singleton.shared_variable)

samplemodule2.py

Echemos un vistazo a la salida.

Aquí, el valor cambiado por samplemodule1 también se refleja en samplemodule2.

Singleton clásico:

Classic Singleton crea una instancia solo si no se ha creado ninguna instancia hasta el momento; de lo contrario, devolverá la instancia que ya está creada. Echemos un vistazo al siguiente código.

Python3

class SingletonClass(object):
  def __new__(cls):
    if not hasattr(cls, 'instance'):
      cls.instance = super(SingletonClass, cls).__new__(cls)
    return cls.instance
   
singleton = SingletonClass()
new_singleton = SingletonClass()
 
print(singleton is new_singleton)
 
singleton.singl_variable = "Singleton Variable"
print(new_singleton.singl_variable)
Producción

True
Singleton Variable

Aquí, en el método __new__, comprobaremos si se crea una instancia o no. Si se crea, devolverá la instancia; de lo contrario, creará una nueva instancia. Puede notar que singleton y new_singleton devuelven la misma instancia y tienen la misma variable.

Veamos qué sucede cuando subclasificamos una clase singleton. 

Python3

class SingletonClass(object):
  def __new__(cls):
    if not hasattr(cls, 'instance'):
      cls.instance = super(SingletonClass, cls).__new__(cls)
    return cls.instance
   
class SingletonChild(SingletonClass):
    pass
   
singleton = SingletonClass() 
child = SingletonChild()
print(child is singleton)
 
singleton.singl_variable = "Singleton Variable"
print(child.singl_variable)
Producción

True
Singleton Variable

Aquí puede ver que SingletonChild tiene la misma instancia de SingletonClass y también comparte el mismo estado. Pero hay escenarios, donde necesitamos una instancia diferente, pero deberíamos compartir el mismo estado. Este estado compartido se puede lograr utilizando Borg singleton.

Borg Singleton: 

Borg singleton es un patrón de diseño en Python que permite compartir estados para diferentes instancias. Analicemos el siguiente código.

Python3

class BorgSingleton(object):
  _shared_borg_state = {}
   
  def __new__(cls, *args, **kwargs):
    obj = super(BorgSingleton, cls).__new__(cls, *args, **kwargs)
    obj.__dict__ = cls._shared_borg_state
    return obj
   
borg = BorgSingleton()
borg.shared_variable = "Shared Variable"
 
class ChildBorg(BorgSingleton):
  pass
 
childBorg = ChildBorg()
print(childBorg is borg)
print(childBorg.shared_variable)
Producción

False
Shared Variable

Junto con el nuevo proceso de creación de instancias, también se define un estado compartido en el método __new__. Aquí, el estado compartido se conserva mediante el atributo shared_borg_state y se almacena en el diccionario __dict__ de cada instancia.

Si desea un estado diferente, puede restablecer el atributo shared_borg_state. Veamos cómo restablecer un estado compartido.

Python3

class BorgSingleton(object):
  _shared_borg_state = {}
   
  def __new__(cls, *args, **kwargs):
    obj = super(BorgSingleton, cls).__new__(cls, *args, **kwargs)
    obj.__dict__ = cls._shared_borg_state
    return obj
   
borg = BorgSingleton()
borg.shared_variable = "Shared Variable"
 
class NewChildBorg(BorgSingleton):
    _shared_borg_state = {}
 
newChildBorg = NewChildBorg()
print(newChildBorg.shared_variable)

Aquí, hemos restablecido el estado compartido e intentamos acceder a shared_variable. Veamos el error.

Traceback (most recent call last):
  File "/home/329d68500c5916767fbaf351710ebb13.py", line 16, in <module>
    print(newChildBorg.shared_variable)
AttributeError: 'NewChildBorg' object has no attribute 'shared_variable'

Casos de uso de un Singleton:

Enumeremos algunos de los casos de uso de una clase singleton. Son los siguientes:

  • Administrar una conexión de base de datos
  • Punto de acceso global para escribir mensajes de registro
  • Administrador de archivos
  • Cola de impresión

Cree un rastreador web usando Classic Singleton:

Vamos a crear un rastreador web que use el beneficio de un singleton clásico. En este ejemplo práctico, el rastreador escanea una página web, busca los enlaces asociados con el mismo sitio web y descarga todas las imágenes que contiene. Aquí, tenemos dos clases principales y dos funciones principales.

  • CrawlerSingleton: esta clase actúa como un singleton clásico
  • ParallelDownloader: esta clase proporciona funcionalidad de subprocesos para descargar imágenes
  • navegar_sitio: esta función rastrea el sitio web y obtiene los enlaces que pertenecen al mismo sitio web. Y, por último, dispone el enlace para descargar imágenes.
  • download_images: esta función rastrea el enlace de la página y descarga las imágenes.

Además de las clases y funciones anteriores, utilizamos dos conjuntos de bibliotecas para analizar la página web: BeautifulSoap y HTTP Client .

Echa un vistazo al siguiente código. 

Nota: Ejecute el código en su máquina local

Python3

import httplib2
import os
import re
import threading
import urllib
import urllib.request
from urllib.parse import urlparse, urljoin
from bs4 import BeautifulSoup
 
class CrawlerSingleton(object):
    def __new__(cls):
        """ creates a singleton object, if it is not created,
        or else returns the previous singleton object"""
        if not hasattr(cls, 'instance'):
            cls.instance = super(CrawlerSingleton, cls).__new__(cls)
        return cls.instance
 
def navigate_site(max_links = 5):
    """ navigate the website using BFS algorithm, find links and
        arrange them for downloading images """
 
    # singleton instance
    parser_crawlersingleton = CrawlerSingleton()
     
    # During the initial stage, url_queue has the main_url.
    # Upon parsing the main_url page, new links that belong to the
    # same website is added to the url_queue until
    # it equals to max _links.
    while parser_crawlersingleton.url_queue:
 
        # checks whether it reached the max. link
        if len(parser_crawlersingleton.visited_url) == max_links:
            return
 
        # pop the url from the queue
        url = parser_crawlersingleton.url_queue.pop()
 
        # connect to the web page
        http = httplib2.Http()
        try:
            status, response = http.request(url)
        except Exception:
            continue
         
        # add the link to download the images
        parser_crawlersingleton.visited_url.add(url)
        print(url)
 
        # crawl the web page and fetch the links within
        # the main page
        bs = BeautifulSoup(response, "html.parser")
 
        for link in BeautifulSoup.findAll(bs, 'a'):
            link_url = link.get('href')
            if not link_url:
                continue
 
            # parse the fetched link
            parsed = urlparse(link_url)
             
            # skip the link, if it leads to an external page
            if parsed.netloc and parsed.netloc != parsed_url.netloc:
                continue
 
            scheme = parsed_url.scheme
            netloc = parsed.netloc or parsed_url.netloc
            path = parsed.path
             
            # construct a full url
            link_url = scheme +'://' +netloc + path
 
             
            # skip, if the link is already added
            if link_url in parser_crawlersingleton.visited_url:
                continue
             
            # Add the new link fetched,
            # so that the while loop continues with next iteration.
            parser_crawlersingleton.url_queue = [link_url] +\
                                                parser_crawlersingleton.url_queue
             
class ParallelDownloader(threading.Thread):
    """ Download the images parallelly """
    def __init__(self, thread_id, name, counter):
        threading.Thread.__init__(self)
        self.name = name
 
    def run(self):
        print('Starting thread', self.name)
        # function to download the images
        download_images(self.name)
        print('Finished thread', self.name)
             
def download_images(thread_name):
    # singleton instance
    singleton = CrawlerSingleton()
    # visited_url has a set of URLs.
    # Here we will fetch each URL and
    # download the images in it.
    while singleton.visited_url:
        # pop the url to download the images
        url = singleton.visited_url.pop()
 
        http = httplib2.Http()
        print(thread_name, 'Downloading images from', url)
 
        try:
            status, response = http.request(url)
        except Exception:
            continue
 
        # parse the web page to find all images
        bs = BeautifulSoup(response, "html.parser")
 
        # Find all <img> tags
        images = BeautifulSoup.findAll(bs, 'img')
 
        for image in images:
            src = image.get('src')
            src = urljoin(url, src)
 
            basename = os.path.basename(src)
            print('basename:', basename)
 
            if basename != '':
                if src not in singleton.image_downloaded:
                    singleton.image_downloaded.add(src)
                    print('Downloading', src)
                    # Download the images to local system
                    urllib.request.urlretrieve(src, os.path.join('images', basename))
                    print(thread_name, 'finished downloading images from', url)
 
def main():
    # singleton instance
    crwSingltn = CrawlerSingleton()
 
    # adding the url to the queue for parsing
    crwSingltn.url_queue = [main_url]
 
    # initializing a set to store all visited URLs
    # for downloading images.
    crwSingltn.visited_url = set()
 
    # initializing a set to store path of the downloaded images
    crwSingltn.image_downloaded = set()
     
    # invoking the method to crawl the website
    navigate_site()
 
    ## create images directory if not exists
    if not os.path.exists('images'):
        os.makedirs('images')
 
    thread1 = ParallelDownloader(1, "Thread-1", 1)
    thread2 = ParallelDownloader(2, "Thread-2", 2)
 
    # Start new threads
    thread1.start()
    thread2.start()
 
     
if __name__ == "__main__":
    main_url = ("https://www.geeksforgeeks.org/")
    parsed_url = urlparse(main_url)
    main()

Veamos las imágenes descargadas y la salida de Python Shell.

Imágenes descargadas

Salida de shell de Python

Resumen:

El patrón Singleton es un patrón de diseño en Python que restringe la instanciación de una clase a un objeto. Puede limitar el acceso simultáneo a un recurso compartido y también ayuda a crear un punto de acceso global para un recurso. 

Publicación traducida automáticamente

Artículo escrito por SonuGeorge y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *