How to add class-hierarchy based defaults without inherited attributes

Viewed 38

I have a class, let's call it Animal, defined in one library.

In a separate library, I want to represent these Animals with colors according to their subtype. For example, a Lion object is red. I want there for be a default color for any Animal in case a color isn't one defined for it. I also want there be subtypes of Animal, such as Feline, that can themselves define a default color for supersedes that of generic Animals.

A simple way to achieve something like this would be as follows. (I use Python here to demonstrate, but I believe this to be a language-agnostic problem.)

class Animal:
  def __init__(self):
    self.color = Black

class Feline(Animal):
  def __init__(self):
    super.__init__()
    self.color = Blue

class Dog(Animal):
  pass  # self.color == Black

class Cat(Feline):
  pass  # self.color == Blue

class Lion(Feline):
  def __init__(self):
    super.__init__()
    self.color = Red

Essentially, each Animal will have a color defined by itself or the earliest ancestor that defines it.

However, I don't want the color of an Animal be a field. i.e. the representation of an Animal should be separate from the Animal itself.

So, instead, I am running something more like this:

# animal.py
class Animal:
  pass

class Feline(Animal):
  pass

class Dog(Animal):
  pass

class Cat(Feline):
  pass

class Lion(Feline):
  pass


# animal_colors.py
default_colors: dict[type, Color] = {
  Animal: Black,
  Feline: Blue,
  Lion: Red,
  # Notice how Cat, and Dog aren't defined here
}

def get_color(animal: Animal, lookup_table:dict[type, Color]=default_colors) -> Color:
  mro = type(animal).__mro__
  assert Animal in mro, "Non Animal class passed to get_color()"
  for t in mro:
      try:
          return lookup_table[t]
      except KeyError:
          continue
  raise RuntimeError("Animal object has no defined color")

This way get_color(my_animal) will return the desired color for an animal.

This, however, simply feels like the wrong approach to me... I don't think I should be meddling with the __mro__ for such a simple use case. I'm looking for better ways to tackle this problem of essentially having attributes and defaults be defined outside the class hierarchy.

Last thing I want to mention, for my case, it makes sense to have instances of Animal or Feline, which would traditionally be abstract class. i.e. objects should be able to instantiate at every level of the hierarchy.. Perhaps I could have DefaultAnimal and DefaultFeline subtypes instead, if needed.

*Bonus question: I believe this approach breaks Liskhov Substitution Principle (if a Feline is supposed to be Blue, a Lion shouldn't be Red) is there a better way to approach this problem?

1 Answers

It sounds like what you are looking for are class attributes.

class Animal:
    color = Black

class Feline(Animal):
    color = Blue

class Lion(Feline):
    color = Red

This defines attributes that belong to the class itself, rather than to its instances. They are accessible either through the instance or the class. E.g.

Lion.color == Red # attribute on the class object
Lion().color == Red # atteibute on the instance object

Python does have support for abstract classes, but they are rarely used. Python is a highly pragmatic language that eschews a lot of the formality of strongly typed OO languages like Java, C# or C++. For example, it has no access modifiers, no real information hiding, no interfaces and rarely uses properties except for computed properties. Inheritance is extremely rare in day to day Python programming.

As to the LSP, you aren't in violation, at least the way the principal is normally understood, because you can access the same properties on Lion as on a Feline, and get values of the same type. LSP is generally understood to operate at the level of types, not values.

Related