Easy Types and Generics in Python: the Prototype Pattern

Introduction

The prototype-pattern is a creational design pattern that allows us to create new objects by cloning existing ones, i.e. the existing objects function as a kind of template. This can save time in some cases when for example an object creation involves some heavy calculation or things like network traffic or database queries.

So what does it look like?

A short explanation:

  1. The main part is the Prototype interface. This defines a Clone() method which returns a Prototype. We will see in the implementation why this is important.
  2. There are some concrete classes which implement the Clone() method. In this example there are two, but this could of course be any number.
  3. Finally there is the Client, the class which needs the concrete classes.

Implementation in Python

We will start with these preliminaries:


from typing import Self

In this article you will an explanation about Self, which is the type of the enclosing class of the method. This will come in very handy as you will see.

Next we define the Prototype base class:

class Prototype:
    def clone(self) -> Self:
        pass

The clone() method will return a copy of the class, that is an object of the type of the enclosing class. That is why we have Self as the return type.

Then we come to implementing our prototype. In our example, our prototype holds one value:

class ConcretePrototype[V](Prototype):
    _x: V = None

    def __init__(self, value: V):
        self._x = value

    def clone(self) -> Self:
        return ConcretePrototype(self._x)

    @property
    def x(self) -> V:
        return self._x

Some notes:

  1. The ConcretePrototype is a generic class, since the value it holds can be different types
  2. The value within the type is immutable, that is why we have the @property annotation for the x() method.
  3. In the constructor we initialize _x with the value we pass to the constructor
  4. With the clone() method we construct a new instance of the ConcretePrototype with the existing value, and return it. Remember: Self is the type of the enclosing class, in our case ConcretePrototype

Time to test

The testing code looks like this:

if __name__ == "__main__":
    p1: ConcretePrototype(int) = ConcretePrototype(1)
    p2: ConcretePrototype(int) = p1.clone()
    p3: ConcretePrototype(str) = ConcretePrototype("Hello")
    p4: ConcretePrototype(str) = p3.clone()

    print(f"p1: {p1.x}")
    print(f"p2: {p2.x}")
    print(f"p3: {p3.x}")
    print(f"p4: {p4.x}")

Line by line:

  1. We create a ConcretePrototype with the initial value 1. Note that Python will figure out that generic parameter V is of type int
  2. We clone this instance.
  3. We repeat this for ConcretePrototype which holds a string
  4. Then we print out all four values.

Note that the prototype creates new objects, so p1 and p2 do not refer to the same object.

Conclusion

As you can see this is an easy pattern to implement. This pattern is especially useful if instantiating is expensive in terms of resources.

Using type-hints, generics and being able to refer to the enclosing type using Self really helps in simplifying this implementation.

In a further article I hope to come up with a more complex example, where you can really see what advantages this pattern offers.

Leave a Reply

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