Search code examples
pythonoopmetaprogrammingintrospection

Python: create a subclass dynamically in class body


In the following snippet I'm trying to define a factory function which would return objects of different classes derived from Hero based on arguments.

class Hero:
    Stats = namedtuple('Stats', ['health', 'defence', 'attack',
                                 'mana', 'experience'])
    RaceMaxStats = OrderedDict([
        ('Knight', Stats(100, 170, 150, 0, inf)),
        ('Barbarian', Stats(120, 150, 180, 0, inf)),
        ('Sorceress', Stats(50, 42, 90, 200, inf)),
        ('Warlock', Stats(70, 50, 100, 180, inf))
    ])

    @staticmethod
    def def_race(race: str):
        return type(race, (Hero,), {'max': Hero.RaceMaxStats[race]})

    Races = OrderedDict([
        (race, Hero.def_race(race)) for race in RaceMaxStats.keys()
    ])

    def __init__(self, lord, health, defence, attack, mana, experience):
        self.race = self.__class__.__name__
        self.lord = lord
        self.stats = Hero.Stats(min(health, self.max.health),
                                min(defence, self.max.defence),
                                min(attack, self.max.attack),
                                min(mana, self.max.mana),
                                min(experience, self.max.experience))

    @staticmethod
    def summon(race, *args, **kwargs):
        return Hero.Races[race](*args, **kwargs)

With the intention of later using it like so:

knight = Hero.summon('Knight', 'Ronald', 90, 150, 150, 0, 20)
warlock = Hero.summon('Warlock', 'Archibald', 50, 50, 100, 150, 50)

The problem is that I cannot initialize the subclasses because Hero is not yet defined:

    (race, Hero.def_race(race)) for race in RaceMaxStats.keys()
NameError: name 'Hero' is not defined

Obviously, if I replaced the static method call with the direct type() call I would still need Hero to be defined. My question is how do I best implement this kind of factory. The priority is for the summon() method to retain the same signature, and to return instances of classes derived from Hero.

P.S. none of the code above has ever been run successfully, so it may contain other mistakes.


Solution

  • You can use classmethods and define yours Races variable as a method which caches its result after its first call in a class variable. It would look like that:

    @classmethod
    def def_race(cls, race: str):
        return type(race, (cls,), {'max': cls.RaceMaxStats[race]})
    
    _Races = None
    
    @classmethod
    def Races(cls, race):
        if cls._Races is None:
            cls._Races = OrderedDict([
               (race, cls.def_race(race)) for race in cls.RaceMaxStats.keys()
            ])
        return cls._Races[race]
    
    @classmethod
    def summon(cls, race, *args, **kwargs):
        return cls.Races(race)(*args, **kwargs)