Prototype design pattern in Python

It is one of the creational design pattern methods. It is used when we have to reduce the number of classes. It aims to reduce the number of classes. Here we copy the existing object rather than creating one. It has the facility of creating an object independent of the actual implementation. Creating an object requires resources and time. Sometimes it's expensive, too, so we use a prototype design pattern during run time to copy the object and use it as a new one. The major benefit is even if we are copying the actual object, we can modify it as per our requirement as it clones the existing object. That is why its name is a prototype design pattern, commonly used in the expensive operation of the database. You do not need to have depth knowledge of the underlying structure to use it.

Definition: Prototype design patterns generally clone the existing design objects, even complex ones, and coupling to class is not needed.

Example 1:

Suppose we have an emoji class that provides us with different emojis such as happy, sad, laugh and anger, etc. now, for some reason, you have to make the same object, so what you will do? Will you make emoji object from scratch? You might do so! But think, why waste resources and time if you already have the same object, so you will be copying that object and then putting it where it's needed rather than creating it again.

Example 2

Suppose you are a graphic designer and you deliver a lot of projects to clients, so you have created a template class where it has all the necessary prototypes of shapes. Now a client wants a star-shaped icon, so now the decision is yours! What will you do? Will you create the star icon from scratch? or will you clone the existing star icon so the answer is crystal clear? You will be cloning the existing star icon and then modifying its attribute according to the client's needs.

Let's understand it by practical example :

Example.py

# concrete course
class math():
"""Class for maths and neumericals"""


def Type(self):
return "maths and neumericals"


def __str__(self):
return "math"




# concrete course
class SE():
"""Class for Software debugger engineer"""


def Type(self):
return "Software debugger engineer"


def __str__(self):
return "SE"




# concrete course
class SST():
"""class for history and geography"""


def Type(self):
return "history and geography"


def __str__(self):
return "SST"


# concrete course
class sceince():
"""Class for science and innovation """


def Type(self):
return "science and innovation"


def __str__(self):
return "science"
# concrete course
class physics():
"""Class applied physics"""


def Type(self):
return "applied physics"


def __str__(self):
return "physics"
# concrete course
class chemistry():
"""Class chemistry and practical of it"""


def Type(self):
return "chemistry and practical of it"


def __str__(self):
return "chemistry"


# main method
if __name__ == "__main__":
se = SE() # object for SE
math = math() # object for math
sst = SST() # object for SST
chem = chemistry()
phy = physics()


print(f'Name of Course: {se} and its type: {se.Type()}')
print(f'Name of Course: {math} and its type: {math.Type()}')
print(f'Name of Course: {sst} and its type: {sst.Type()}')
print(f'Name of Course: {chem} and its type: {chem.Type()}')
print(f'Name of Course: {phy} and its type: {phy.Type()}')

Output.txt:

Name of Course: SE and its type: Software debugger engineer
Name of Course: math and its type: maths and neumericals
Name of Course: SST and its type: history and geography
Name of Course: chemistry and its type: chemistry and practical of it
Name of Course: physics and its type: applied physics

Explanation :

Here we have created six concrete classes for objects. You can see all of them follow a similar pattern. Still, even if I want to create an object of a similar pattern, I have to make new objects again and again, which has made the code bulky, so this is the problem with the conventional method that will be resolved using the prototype method, in prototype method rather than creating a new object of similar pattern we will be cloning the existing pattern and then we will be making changes in the cloned object as per our need so let's see how we can do this.

Example 1

Protoexample.py

# import the required modules
from abc import ABCMeta, abstractmethod
import copy
# class - Courses at our tuition center
class Courses_At_center(metaclass = ABCMeta):
   # constructor
  def __init__(self):
    self.id = None
    self.type = None
 @abstractmethod
  def course(self):
    pass
  def get_type(self):
    return self.type
 def get_id(self):
    return self.id


  def set_id(self, sidi):
    self.id = sidi
 def clone(self):
    return copy.copy(self)
# class - math course
class math(Courses_At_center):
  def __init__(self):
    super().__init__()
    self.type = "mathematics and numericals"
 def course(self):
    print("Inside math::course() method")
# class - SE Course
class SE(Courses_At_center):
  def __init__(self):
    super().__init__()
    self.type = "Software Debugger Engineer"
def course(self):
    print("Inside SE::course() method.")
# class - SST Course
class SST(Courses_At_center):
  def __init__(self):
    super().__init__()
    self.type = "general studies "
  def course(self):
    print("Inside SST::course() method.")
# class - Courses At GeeksforGeeks Cache
class Courses_At_center_Cache:
    # cache to store useful information
  cache = {}
 @staticmethod
  def get_course(sidi):
    COURSE = Courses_At_center_Cache.cache.get(sidi, None)
    return COURSE.clone()
if __name__ == '__main__':
  Courses_At_center_Cache.load()
  sde = Courses_At_center_Cache.get_course("1")
  print(sde.get_type())
maths = Courses_At_center_Cache.get_course("2")
print(maths.get_type())


sst = Courses_At_center_Cache.get_course("3")
print(sst.get_type())
@staticmethod
  def load():
    se = SE()
    se.set_id("1")
    Courses_At_center_Cache.cache[sde.get_id()] = sde
   maths = math()
    maths.set_id("2")
    Courses_At_center_Cache.cache[maths.get_id()] = maths
    sst = SST()
    sst.set_id("3")
    Courses_At_center_Cache.cache[sst.get_id()] = sst
# main function
if __name__ == '__main__':
  Courses_At_center_Cache.load()
 sde = Courses_At_center_Cache.get_course("1")
  print(sde.get_type())
maths = Courses_At_center_Cache.get_course("2")
print(maths.get_type())
sst = Courses_At_center_Cache.get_course("3")
print(sst.get_type())

Output.txt :

Software Debugger Engineer
mathematics and numericals
general studies 

Explanation:

So, first of all, we are importing all the required modules. Here we need ABCmeta from this. We are importing the abstract method to clone objects. we have to extract values from existing objects and then have defined constructors for cloning the objects.

Constructor:

def __init__(self):
    self.id = None
    self.type = None
@abstractmethod
  def course(self):
    pass
def get_type(self):
    return self.type

This is one of the created constructors. As you know, constructors are used to initializing the object, so here we have made respective constructors for each object so that we can initialize as well as access them when we want, and this enables cloning too because for cloning an object, first of all, we need to access it and then we can perform the necessary actions. In the above code, only one constructor is there. You can check example.py for other created constructors.

  def course(self):
    print("Inside math::course() method")

You can see in the last example we were passing values to each object, but here we do not have to do that. Our main object, couse_at_center, is passed to each class to enable the cloning, and we are calling the constructor rather than defining values. We can call the constructor using the super method, and then the constructor will instantiate the values; we are instantiating it with the type of value, and lastly, we are calling the course method to print the value, all the existing classes are of the same patterns so this same method will be used for instantiation.

We are creating cache to store important information.

 cache = {}
@staticmethod
  def get_course(sidi):
    COURSE = Courses_At_center_Cache.cache.get(sidi, None)
    return COURSE.clone()
if __name__ == '__main__':
  Courses_At_center_Cache.load()

The Cache will be storing important information and will return it when needed. You can see the clone method is used here. Then we provide the id to each Cache, and then we will print the values that are in the output. We are just printing the type using the get type method. The Cache is used for quick access to information so that you can store the most frequently called values. This will be less time-consuming. We provide an id to each class, and then while printing the value, we are not calling the object directly. We are using Cache and passing the respective id, and then the value gets fetched and stored to Cache, and then it is printed. When we want to create a new object, we can easily clone the existing ones and then manipulate them as per our requirements.

Example - 2

Cloning_example.py

class bike:
        def __init__(self, engine="2500cc", color="f-white", seats=2):
                self.engine = engine
                self.color = color
                self.seats = seats
        def __str__(self):
                return  f'{self.engine} | {self.color} | {self.seats}'
import copy
class Prototype:
        def __init__(self):
      
                self._Cloned_Objects = {}


        def RegisterObject(self, name, obj):
                
                self._Cloned_Objects[name] = obj


        def DeregisterObject(self, name):
             
                del self._Cloned_Objects[name]


        def Clone(self, name, **kwargs):


               clonedObject = copy.deepcopy(self._Cloned_Objects.get(name))
               clonedObject.__dict__.update(kwargs)
               return clonedObject
if __name__ == "__main__":


  defaultbike = bike()
prototype = Prototype()


CarType1 = bike("1000cc", "Red", 4)


prototype.RegisterObject('Basicbike', defaultbike)  
prototype.RegisterObject('Type-1', CarType1)


bikeOne = prototype.Clone('Basicbike', color = "dull brown")                                                      
bikeTwo = prototype.Clone('Type-1',color = "blue") 
bikeThree = prototype.Clone('Type-1', color = "royal black")


print("Details of the default-bike:", defaultbike)  
print("Details of bike-One:", bikeOne)        
print("Details of bike-Two:", bikeTwo)  
print("Details of bike-Three:", bikeThree)  

Output.txt :

Details of the default-bike: 2500cc | f-white | 2
Details of bike-One: 2500cc | dull brown | 2
Details of bike-Two: 1000cc | blue | 4
Details of bike-Three: 1000cc | royal black | 4

Explanation: This is an interesting example where we are cloning the object so let's try to understand this from scratch. So the first step is the creation of a class.

    def __init__(self, engine="2500cc", color="f-white", seats=2):
                self.engine = engine
                self.color = color
                self.seats = seats

 This is our default object, and the class name is bike, And then, you can see three attributes engine, color, and seats. These three attributes will be cloned, so we have to make a dictionary where the cloned object will be stored.

we have made one dictionary, and we will perform two operations.  Dictionary will be used for cloning values, the first method will store the cloned values, and the second deletes the cloned values.

  •  Dictionary for storing clone object that is - self._Cloned_Objects = {}
  • Method to store all existing clones -  self._Cloned_Objects[name] = obj
  • Method to delete all existing clones -  del self._Cloned_Objects[name]

So all the preparation for cloning is done. Now we can clone the object and manipulate it. we have created three clones, bike one, bike two, and bike three.

bikeOne = prototype.Clone('Basicbike', color = "dull brown")                                                      
bike to = prototype.Clone('Type-1',color = "blue") 
bikeThree = prototype.Clone('Type-1', color = "royal black")

As you can see, we are not creating a specific class for the three bikes. We are just cloning the default bike and then manipulating the values, which has made our code super simple and less bulky. This makes our code efficient, and professional developers use this.

Note-If you will not provide any attributes to the cloned object, then it will take the values of the default class

Advantages of using prototype design pattern :

  • This enables adding and deleting objects at run time. We add new objects if needed and delete them if they are no longer in use, so this is very useful in terms of time and performance because there is no availability of unused objects.
  • This design pattern has highly dynamic behavior. We can manipulate the values very easily.
  • In factory method, it consists of various sub-classes. Still, in this design pattern method, you need to clone the prototype design pattern rather than ask the factory to generate one new object.
  • Code is simple and easy to understand

The disadvantage of prototype design pattern :

  • The concrete class is not visible to the client.
  • One major drawback is that each subclass must perform the clone operation, which is sometimes complex.
  • Not useful for a project with few objects. In that case, you can go for the conventional method.

So this was a brief introduction to prototype design patterns. We will be discussing more design patterns in the upcoming parts of this tutorial.