Easy Implementation of Python’s Lazy Initialization Pattern for Improved Efficiency

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:

  1. The client object.
  2. The object proxy that handles object creation.
  3. 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:

  1. We start by defining our two attributes, a dictionary with a string as key and an ExpensiveCar as value and a threading.Lock object
  2. 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:

  1. We create an ExpensiveCarGarage
  2. Next we create the three threads, getting a red, blue and a red car again
  3. We start the thread, by calling their start() methods
  4. We the join() methods to wait for the threads to finish their execution
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *