Design Patterns with Python for Machine Learning Engineers: Prototype

Introduction
This is not the first post blog I am writing about design patterns. In my recent posts, I've received positive feedback on this topic because apparently using design patterns is not a common practice in the Python world. I think people should learn these patterns to enhance and improve their code. Moreover, today AI software is heavily based on Python, so I think that these tutorials are useful to all the people dealing with AI. I will run my code on the Deepnote: which is a cloud-based notebook that's great for collaborative Data Science projects.
What is a design pattern?
Design Patterns provide well-defined solutions to problems that recur very often when designing software. Rather than solve the same issue over and over again, these patterns offer reusable solutions, accelerating the whole development process.
Design patterns essentially provide a robust and tested blueprint to address specific problems optimally, making our lives easier.
There are various types of design patterns, generally categorized into three groups:
- Creational Patterns: These focus on the creation of objects, providing mechanisms for object creation while keeping the system flexible and efficient.
- Structural Patterns: They revolve around the composition of classes and objects, dealing with the relationships between different components to form larger structures.
- Behavioural Patterns: This category governs how classes and objects interact, outlining the distribution of responsibilities among them. It defines protocols for communication and collaboration within a software system."

The Problem
When we work on big projects using Python, we generally adopt an object-oriented Programming methodology to make the code more readable. Usually, we end up having a lot of classes and tons of objects.
Sometimes what happens is that we want to create an exact copy of an object. How do you do that? In a naive manner, you can instantiate another object of the same class and then copy each internal field of the object you want to clone. But this process is slow and boring.
Moreover, there could be another problem. Sometimes you cannot instantiate an object that easily, because calling the constructor of a class can be costly. Think for example the case in which in the constructor you run an API request to an external service that you pay.
How can we solve this? Well… with design patterns, in particular with a creational one: the prototype.
The Prototype Design Pattern
First of all, we need to create an abstract class (or interface) with the clone() abstract method. All the classes that we create will then have to instantiate this interface and define how to clone an object of the class itself. In this way, the duty of creating a clone is not of the class but of the object itself.

I am now going to create an abstract class by using the ABC Python library.
The following class will define a vehicle prototype with some attributes.
from abc import ABC, abstractmethod
import time
# Class Creation
class Prototype(ABC):
# Constructor:
def __init__(self):
# Mocking an expensive call
time.sleep(2)
# Base attributes
self.color = None
self.wheels = None
self.velocity = None
# Clone Method:
@abstractmethod
def clone(self):
pass
Now we can create some class that will implement this interface, and to do that in particular they have to implement the clone() abstract class. In Python in order to create a copy we can use the deepcopy() or a shallow copy() method of the copy library.
To be specific, a shallow copy duplicates references to non-primitive fields, while a deep copy generates new instances with identical data.
Let's now define a concrete class. I am going to make the constructor sleep for 2 seconds to simulate the costly call of the constructor as explained in the introduction.
import copy
import time
class RaceCar(Prototype):
def __init__(self, color, wheels, velocity, attack):
super().__init__()
# Mock expensive call
time.sleep(2)
self.color = color
self.wheels = wheels
self.velocity = velocity
# Subclass-specific Attribute
self.acceleration = True
# Overwriting Cloning Method:
def clone(self):
return copy.deepcopy(self)
Every time that I want to instantiate a RaceCar object it is going to take 2 seconds, because of the sleep method. We can monitor this.
import time
start = time.time()
print('Starting to create a race car.')
race_car = RaceCar("red", 4, 40)
print('Finished creating a race car', )
end = time.time()
#will take 2 seconds
print('Time to complete: ' , end-start)
Nice! We created a race car, wasn't that hard. Now I want to make 5 copies of the car because my purpose is to develop a video game with a lot of cars. We can simply do the same iterating in a for loop.
cars = []
start = time.time()
print('Start instantiating clones', )
for i in range(5):
race_car = RaceCar("red", 4, 40)
cars.append(race_car)
end = time.time()
print('Time to complete: ', end-start)
*If you run this code it will take 2s5 so 10s in total!**