Search code examples
pythonoopdiamond-problem

How to avoid parallel class hierarchy in python


I've been running into a weird little smell in my python code lately and I think it has something to do with parallel inheritance. Here is a small example I concocted:

class DogHabits:
    def __init__(self):
        self.habits = ['lick butt']

class GermanShepherdHabits(DogHabits):
    def __init__(self):
        super().__init__()
        self.habits.extend(['herd sheep'])

class LabradorHabits(DogHabits):
    def __init__(self):
        super().__init__()
        self.habits.extend(['hunt', 'pee on owner'])

class Dog:
    def __init__(self):
        self.type = 'generic_dog'
        self.my_habits = DogHabits()

    def do_stuff(self):
        for habit in self.my_habits.habits:
            print(habit)

class GermanShepherd(Dog):
    def __init__(self):
        self.type = 'german shepherd'
        self.my_habits = GermanShepherdHabits()

class Labrador(Dog):
    def __init__(self):
        self.type = 'labrador'
        self.my_habits = LabradorHabits()

if __name__ == "__main__":
    german_shepherd = GermanShepherd()
    print('\n{}'.format(german_shepherd.type))
    german_shepherd.do_stuff()


    labrador = Labrador()
    print('\n{}'.format(labrador.type))
    labrador.do_stuff()

I have a generic dog class from which concrete dog implementations inherit. Every dog class (including the generic/abstract one) has a set of habits, itself represented by another class hierarchy for the habits.

I am annoyed by the fact that I have to have both hierarchies exactly the same at all times. Furthermore the inheritance between the DogHabits is useful within the habits hierarchy, but it is not useful within the dogs hierarchy, as I need to instantiate a separate habits object for each class in the dog hierarchy.

What is the antidote to this? I may want to add many implementations of the dog class, and updating the corresponding habits hierarchy sounds tedious and smells bad...


Solution

  • IF habits need to a class attribute, rather than instance attributes, this may actually be a good use for metaclasses.

    Habits need not be a simple list, it could be something else, as long as there is the notion of addition to previous and return new. (__add__ or __radd__ on a Habits class would do the trick I think)

    class DogType(type):
    
        def __init__(cls, name, bases, attrs):
            """ this is called at the Dog-class creation time.  """
    
            if not bases:
                return
    
            #pick the habits of direct ancestor and extend it with 
            #this class then assign to cls.
            if "habits" in attrs:
                base_habits = getattr(bases[0], "habits", [])
                cls.habits = base_habits + cls.habits
    
    
    class Dog(metaclass=DogType):
        habits = ["licks butt"]
    
        def __repr__(self):
            return f"My name is {self.name}.  I am a {self.__class__.__name__} %s and I like to {self.habits}"
    
        def __init__(self, name):
            """ dog instance can have all sorts of instance variables"""
            self.name = name
    
    class Sheperd(Dog):
        habits = ["herds sheep"]
    
    class GermanSheperd(Sheperd):
        habits = ["bites people"]
    
    class Poodle(Dog):
        habits = ["barks stupidly"]
    
    class StBernard(Dog):
        pass
    
    
    for ix, cls in enumerate([GermanSheperd, Poodle, StBernard]):
        name = f"dog{ix}"
        dog = cls(name)
        print(dog)
    

    output:

    My name is dog0.  I am a GermanSheperd %s and I like to ['licks butt', 'herds sheep', 'bites people']
    My name is dog1.  I am a Poodle %s and I like to ['licks butt', 'barks stupidly']
    My name is dog2.  I am a StBernard %s and I like to ['licks butt']