Thursday, 30 June 2022

Create Child Instance from Parent Instance

Recently I was thinking for a while about how better design this case (in python). I have a Base class and a Child class and I want to create a Child instance from an existing Base instance. I ended up with this design


import json

class City:
    def __init__(self, name, population):
        self.name = name
        self.population = population
        self.neighbourhoods = []

    def add_neighbourhood(self, name):
        self.neighbourhoods.append(name)

class FrenchCity(City):
    def __init__(self, name, population, urban_area_population):
        super().__init__(name, population)
        self._initialize(urban_area_population)

    def _initialize(self, urban_area_population):
        self.urban_area_population = urban_area_population
        self.related_cities = []

    def add_related_city(self, city):
        self.related_cities.append(city)

    @classmethod
    def create_from_city(cls, city, urban_area_population):
        # I'm doing object.__new__ rather than calling FrenchCity constructor cause in other classes 
        french_city = object.__new__(cls)
        french_city.__dict__.update(city.__dict__)
        french_city._initialize(urban_area_population)
        return french_city


city = City("Paris", 2000000)
city.add_neighbourhood("Belleville")
f_city = FrenchCity.create_from_city(city, 12000000)
f_city.add_related_city("Saint Mande")
print(json.dumps(f_city, indent=4, default=lambda x: x.__dict__))

So as you can see I have added a create_from_[parent] static method to the child class. In it I assign the data in the parent instance __dict__ to the child instance __dict__. Yes, I know that direct access to __dict__ should be avoided as it's mainly an implementation detail, but at the same time it's well known and I don't think it's going to change in the near future.

The other interesting part in the create method is that rather than invoking the child constructor, I'm calling object.__new__ (that's also why I've split the initialization functionality between the normal __init__ method and an _initialize method) rather than the normal Child "constructor" (that ends up calling both __new__ and __init__). In this particular class I could use that "constructor" cause it's clear what parameters to pass from the parent instance fields, but this is not always the case, so the generic way is to create an instance of the child class using __new__, then initialize the child properties with an _initialize method (that we also use when creating the instance from scratch rather than from an existing parent instance), and then set the properties from the parent instance using __dict__

There's a gotcha. We could have a class that were using __slots__ rather than __dict__. Adapting to that should not be a big deal, but I would have to review how __slots__ work.

No comments:

Post a Comment