Introduction
Sometimes, making something can be costly. It might take a lot of resources or time to create an object, especially if it depends on other things like web services or databases.
In such cases, it’s a good idea to create the object only when you really need it. This is where the lazy initialization pattern comes in. It’s a way to delay creating expensive objects until they are required.
You might think it’s similar to the singleton pattern, but there’s a difference. In a singleton, you have only one instance of an object throughout the application’s lifetime. With lazy initialization, you can create many of these expensive objects, each with different states.
If you want to manage multiple instances of expensive objects, you can use the multiton pattern. It’s similar to a singleton, but it allows limited instances of an object, and you can control and create them using a map or dictionary.
Lazy initialization looks like this:
Lazy initialization involves three main components:
- The client object.
- The object proxy that handles object creation.
- The expensive object itself.
The multiton pattern is simpler. It involves managing limited instances of objects through a map or dictionary.
Implementation in Python
Now, let’s see how this works in Python with a thread-safe example. We’ll use a threading lock to ensure that our object is accessed safely by multiple threads.
First, we define an ExpensiveCar class. These cars are identified by their color.
class ExpensiveCar:
_color: str = None
def __init__(self, color: str):
self._color = color
@property
def color(self) -> str:
return self._color
Next, we create an ExpensiveCarGarage class. It uses a dictionary to store cars with their colors as keys and a threading lock to control access to the data.
class ExpensiveCarGarage:
_car_collection: dict[str, ExpensiveCar] = None
_lock: threading.Lock = threading.Lock()
def __init__(self):
self._car_collection = {}
def get_car(self, color: str):
self._lock.acquire()
car: ExpensiveCar = self._car_collection.get(color)
if car is None:
car = ExpensiveCar(color)
print(f"Car with color {color} created")
self._car_collection[color] = car
self._lock.release()
print(f"Got car with color {color}")
A short summary:
- We start by defining our two attributes, a dictionary with a string as key and an ExpensiveCar as value and a threading.Lock object
- In the get_car() method we get the car of the specified. If the color is not available, we create a car of the desired color
The get_car() method
In the get_car()
method, we acquire a lock to ensure only one thread can access this code and data at a time. Then, we try to get the car from the collection. If it’s not there, we create a car of the desired color and add it to the collection. Finally, we release the lock and print out the found or created car.
Testing time
We can test this setup by creating a garage, starting three threads to get red and blue cars, and waiting for the threads to finish.
if __name__ == "__main__":
garage: ExpensiveCarGarage = ExpensiveCarGarage()
red_thread = threading.Thread(target=garage.get_car, args=("red",))
blue_thread = threading.Thread(target=garage.get_car, args=("blue",))
second_red_thread = threading.Thread(target=garage.get_car, args=("red",))
red_thread.start()
blue_thread.start()
second_red_thread.start()
red_thread.join()
blue_thread.join()
second_red_thread.join()
print("Done!")
Line by line:
- We create an ExpensiveCarGarage
- Next we create the three threads, getting a red, blue and a red car again
- We start the thread, by calling their start() methods
- We the join() methods to wait for the threads to finish their execution
- Lastly we print ‘Done!’
Conclusion
In conclusion, lazy initialization is helpful for situations where creating something is expensive. By combining it with the multiton pattern, you can create a flexible solution to manage and control these expensive objects efficiently.