Singleton Design Pattern in Python: A Detailed Guide

The Singleton design pattern is a software design principle that ensures a class has only one instance and provides a global point of access to that instance. This pattern is particularly useful for managing shared resources, like configurations, logging, or database connections.

In this article, we will explore the concept of Singletons in Python, learn about various implementation techniques, and examine real-world use cases.

Why Use Singletons?

Key Characteristics:

  1. Single Instance: Ensures that a class has only one instance.
  2. Global Access Point: Provides a way to access that single instance globally.
  3. Lazy Initialization: The instance is created only when needed.

Common Use Cases:

  • Configuration Managers
  • Logging Services
  • Database Connections
  • Thread Pools

Implementing Singletons in Python

Python offers several ways to implement the Singleton pattern. Let’s dive into each approach.

Classic Singleton Using a Class Variable

In this approach, the class itself manages the single instance using a class variable.

class Singleton:
    _instance = None

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

# Testing the Singleton
obj1 = Singleton()
obj2 = Singleton()

print(obj1 is obj2)  # Output: True

Output:

Explanation:

  • The __new__ method ensures that only one instance of the class is created.
  • If an instance already exists, it returns the existing instance.

Singleton Using a Decorator

A decorator can transform any class into a Singleton.

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 SingletonClass:
    pass

# Testing the Singleton
obj1 = SingletonClass()
obj2 = SingletonClass()

print(obj1 is obj2)  # Output: True

Inner Function: get_instance

  • Parameters: Accepts *args and **kwargs to pass any arguments to the class constructor.
  • Logic:
    1. Check if Instance Exists: The function checks if the class cls is already in the instances dictionary.
    2. Create Instance if Missing: If the class is not present, a new instance is created and stored in instances.
    3. Return Instance: If the class already exists in instances, it simply returns the stored instance.

Application of the Decorator

  • The @singleton decorator is applied to the SingletonClass class:
@singleton
class SingletonClass:
    pass

This means any attempt to create an object of SingletonClass will invoke the get_instance function, enforcing the Singleton behavior.

Testing the Singleton

  • obj1 and obj2 are created as instances of SingletonClass.
  • Singleton Verification: The comparison obj1 is obj2 evaluates to True because both obj1 and obj2 point to the same instance stored in the instances dictionary.

Singleton Using a Metaclass

A metaclass can enforce the Singleton behavior at the class level

class SingletonMeta(type):
    _instances = {}

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

class Singleton(metaclass=SingletonMeta):
    pass

# Testing the Singleton
obj1 = Singleton()
obj2 = Singleton()

print(obj1 is obj2)  # Output: True

Module as a Singleton

In Python, modules are inherently Singletons. When you import a module, it is initialized only once per Python process.

# my_singleton.py
class MySingleton:
    def __init__(self):
        self.value = 42

singleton = MySingleton()

# main.py
from my_singleton import singleton

print(singleton.value)  # Output: 42

Real-World Example: Logger Singleton

Let’s implement a logging service using the Singleton pattern.

import threading

class Logger:
    _instance = None
    _lock = threading.Lock()  # To make it thread-safe

    def __new__(cls, *args, **kwargs):
        with cls._lock:
            if not cls._instance:
                cls._instance = super(Logger, cls).__new__(cls, *args, **kwargs)
        return cls._instance

    def __init__(self):
        if not hasattr(self, "logs"):
            self.logs = []

    def log(self, message):
        self.logs.append(message)
        print(f"Log: {message}")

# Testing the Logger Singleton
logger1 = Logger()
logger2 = Logger()

logger1.log("First log message.")
logger2.log("Second log message.")

print(logger1.logs)  
print(logger1 is logger2)  

Output:

Let’s check what happens when we subclass a singleton class. Python

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)

In the above code you can see that the SingletonChild has the same instance of SingletonClass and also shares the same state. But there are scenarios, where we need a different instance, but should share the same state. This state sharing can be achieved using Borg singleton.

Borg Singleton: 

Borg singleton is a design pattern in Python that allows state sharing for different instances. Let’s look into the following code.

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)
False
Shared Variable

Along with the creation of the new instance, a shared state is defined in the __new__ method. In this case, the shared state is stored in the shared_borg_state attribute, and it is maintained across all instances by being stored in the __dict__ attribute of each instance.

If you want to change the state, you can reset the shared_borg_state attribute. Let’s explore how to reset the shared state.

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)

Here, we have reset the shared state and tried to access the shared_variable. Let’s see the error.

Building a Web Crawler with the Classic Singleton Pattern

In this example, we’ll build a web crawler that utilizes the Classic Singleton pattern. The crawler will scan a webpage, collect all the links related to the same website, and download any images it finds. This implementation includes two main classes and two key functions.

  • CrawlerSingleton: Implements the classic Singleton pattern to ensure only one instance of the crawler.
  • ParallelDownloader: Uses threading to download images in parallel.
  • navigate_site: Crawls the website to find and retrieve links belonging to the same domain, organizing them for image downloading.
  • download_images: Crawls each individual link and downloads the images.

Additionally, we use two popular libraries to parse the web page: BeautifulSoup for parsing HTML, and an HTTP Client for making web requests.

Here’s the code to implement this:

Note: Be sure to run this on your local machine.

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://codemagnet.in/")
    parsed_url = urlparse(main_url)
    main()

Output:

How the Output Is Saved:

  • The download_images function downloads the images in parallel by utilizing threading. When an image is found on a webpage, it checks whether the image has already been downloaded by consulting the singleton.image_downloaded set.
  • If the image has not been downloaded before, it uses urllib.request.urlretrieve to save the image to the local images folder. The image is saved using its basename (the filename portion of the URL).

Where to Find the Saved Images:

  • The images are saved in a folder named images relative to where the script is run. You can find the images in this folder after the script finishes execution.

The output (images) will be saved in the images directory inside your script’s working directory.

Singleton pattern is a design pattern in Python that restricts the instantiation of a class to one object. It can limit concurrent access to a shared resource, and also it helps to create a global point of access for a resource. 

Comparing Singleton Approaches

ApproachProsCons
Class VariableSimple and effectiveNot thread-safe
DecoratorClean and reusableSlightly complex for debugging
MetaclassPowerful and PythonicAdvanced, harder to read
Module as SingletonSimplest and PythonicLimited to module-level Singleton

Conclusion

The Singleton Design Pattern is a powerful structural pattern that ensures a class has only one instance throughout the lifetime of an application. By restricting the instantiation of a class to a single object, it helps in managing shared resources and provides global access to the instance. In Python, implementing the Singleton pattern can be achieved in various ways, including using the __new__ method, decorators, or metaclasses.

Throughout this guide, we explored different approaches to implementing the Singleton pattern and its practical applications. We also examined the benefits, such as controlling resource usage and maintaining consistency across an application. However, while the Singleton pattern offers solutions in certain use cases, it is important to use it judiciously as it can introduce challenges like hidden dependencies and difficulties in testing.

In conclusion, understanding and applying the Singleton Design Pattern effectively can improve the maintainability and efficiency of your Python applications, especially when dealing with shared resources or configurations that need to be accessed globally. As with any design pattern, it is crucial to evaluate its necessity and ensure that it is the right solution for your problem.

Author

Sona Avatar

Written by

Leave a Reply

Trending

CodeMagnet

Your Magnetic Resource, For Coding Brilliance

Programming Languages

Web Development

Data Science and Visualization

Career Section

<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-4205364944170772"
     crossorigin="anonymous"></script>