Design Pattern in Python (1): Singleton

zqiu

Z. QIU

Posted on December 2, 2020

Design Pattern in Python (1): Singleton

Background

It was several years ago that I have learned and used Design Patterns during my previous job. We developed software for medical robots in C++ (i.e OOP) and different design patterns were frequently used in our code.
Later on, when I have started working in our startup, we develop a lot in Python and HTML5 for BigData & Web projects thus we use hardly design patterns.

Recently I am reading this book about Design Patterns as shown below:
Alt Text

This book is very interesting and it makes me recall the knowledge on design patterns. I realize that it's time for me to do some design pattern exercises in Python.

Singleton exercise

So today I tried a simple scenario for implementing Singleton in Python. I want to simulate the case when an user calls siri to play a music item.

GO!

No Singleton Case

Define a regular player class without singleton implementation.

import threading
import time

## a global album dict containing three music as well as their length 
album = {"Love Story":10, "Angels":12, "Elle":18}

### a regular MusicPlayer class which contains a thread for simulating music playing
### 普通的播放器类,自带一个播放线程
class musicplayer():      
    """
        a regular MusicPlayer class 
    """

    def __init__(self):
        self.__currentItem = ""
        self.__musicLength = 0
        self.__timer = 0
        self.__thread = threading.Thread(target=self.playsound)


    def play(self):
        if  self.__thread.isAlive():
            pass
        else:
            self.__thread.start()

    def updateItem(self, musicfile, length):
        self.__currentItem = musicfile
        self.__musicLength = length
        self.__timer = 0

    def playsound(self):

        while self.__timer <= self.__musicLength:
            print(" -- playing   {}  ---- {}.00/{}.00" .format(self.__currentItem, self.__timer, self.__musicLength))
            time.sleep(1)
            self.__timer +=1
        print("{} playing thread has ended".format(self.__currentItem))

Enter fullscreen mode Exit fullscreen mode

Now define a quite simple Siri class which has only one function runCMD for initializing player app and playing music.

class Siri():

    def __init__(self):
        pass

    def runCMD(self, cmd):
        if cmd.startswith("play music"):
            music = cmd.replace("play music", "").strip()
            player = musicplayer()
            player.updateItem(music, album[music])
            player.play()

Enter fullscreen mode Exit fullscreen mode

Now try it in a main function.

if __name__ == "__main__":
    musicList = list(album.keys())
    siri = Siri()
    siri.runCMD("play music {}".format(musicList[0]))
    time.sleep(4)
    siri.runCMD("play music {}".format(musicList[1]))

    mainTime = 0

    while (mainTime < 20):
        mainTime+=1
        time.sleep(1)

    print("End of simulation")
Enter fullscreen mode Exit fullscreen mode

Execute the code above, I see that two music players have been created and they played different items at the same time.
Alt Text

Singleton Case

Now I update a little bit the music player class so that it is a singleton-pattern class. This class is derived from object class so that I need to define __new__() and __init__() functions to do the constructor's work.

class musicplayer(object):      
    """
        ### a SINGLETON MusicPlayer class which contains a thread for simulating music playing
    """
    __instance = None
    __isInstanceDefined = False

    def __new__(cls):
        if not cls.__instance:
            musicplayer.__instance = super().__new__(cls)
        return cls.__instance

    def __init__(self):
        if not musicplayer.__isInstanceDefined:  
            self.__currentItem = ""
            self.__musicLength = 0
            self.__timer = 0
            self.__thread = threading.Thread(target=self.playsound)
            musicplayer.__isInstanceDefined = True
        else:
            pass


    def play(self):
        if  self.__thread.isAlive():
            pass
        else:
            self.__thread.start()

    def updateItem(self, musicfile, length):
        self.__currentItem = musicfile
        self.__musicLength = length
        self.__timer = 0

    def playsound(self):

        while self.__timer <= self.__musicLength:
            print(" -- playing   {}  ---- {}.00/{}.00" .format(self.__currentItem, self.__timer, self.__musicLength))
            time.sleep(1)
            self.__timer +=1
        print("{} playing thread has ended".format(self.__currentItem))
Enter fullscreen mode Exit fullscreen mode

Now execute again the entire code file and it works. There is one and only one player instance in the simulation even siri changed the music item:
Alt Text

Singleton Decorator

In the book I read, the author introduced a very graceful way to realize Singleton pattern without modification of existing classes: using Decorator. I like this method very much.

This decorator method is explicit and simple:

def singletonDecorator(cls, *args, **kwargs):
    """ defines a Singleton decorator """
    instance = {}

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

    return wrapperSingleton
Enter fullscreen mode Exit fullscreen mode

Now I put my entire code file here. Note that there is @singletonDecorator declaration above my existing musicplayer class:

import threading
import time

album = {"Love Story":10, "Angels":12, "Elle":18}


def singletonDecorator(cls, *args, **kwargs):
    """ defines a Singleton decorator """
    instance = {}

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

    return wrapperSingleton

### a regular MusicPlayer class which contains a thread for simulating music playing
### 普通的播放器类,自带一个播放线程
@singletonDecorator
class musicplayer():         
    """
        a regular MusicPlayer class 
    """

    def __init__(self):
        self.__currentItem = ""
        self.__musicLength = 0
        self.__timer = 0
        self.__thread = threading.Thread(target=self.playsound)


    def play(self):
        if  self.__thread.isAlive():
            pass
        else:
            self.__thread.start()

    def updateItem(self, musicfile, length):
        self.__currentItem = musicfile
        self.__musicLength = length
        self.__timer = 0

    def playsound(self):

        while self.__timer <= self.__musicLength:
            print(" -- playing   {}  ---- {}.00/{}.00" .format(self.__currentItem, self.__timer, self.__musicLength))
            time.sleep(1)
            self.__timer +=1
        print("{} playing thread has ended".format(self.__currentItem))

class Siri():

    def __init__(self):
        pass

    def runCMD(self, cmd):
        if cmd.startswith("play music"):
            music = cmd.replace("play music", "").strip()
            player = musicplayer() 
            player.updateItem(music, album[music])
            player.play()



if __name__ == "__main__":


    musicList = list(album.keys())
    siri = Siri()
    siri.runCMD("play music {}".format(musicList[0]))
    time.sleep(4)
    siri.runCMD("play music {}".format(musicList[1]))

    mainTime = 0


    while (mainTime < 20):
        mainTime+=1
        time.sleep(1)

    print("End of simulation")
Enter fullscreen mode Exit fullscreen mode

This new method gives the same result:
Alt Text

💖 💪 🙅 🚩
zqiu
Z. QIU

Posted on December 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related