Why is an omnivore a kind of vegetarian?

An important principle in object oriented design is that you can replace an object of one class with another object of its subclass, and the code should still work. For example, suppose we are creating a calorie counter for various foods. We have a parent class

class Food:
    def __init__(self, name, calories=0):
        self.name = name
        self.calories = calories

and child classes

class Vegetable(Food):
    is_leafy = True

and

class Meat(Food):
    is_bloody = True

Consider a function that returns the calories of an item of food:

def get_calories(food):
    return food.calories

Because a child class inherits the methods and attributes from its parent class - possibly overriding some and adding others - any code that expects Food will also work with Vegetable and Meat.

>>> get_calories(Food('gruel', 1))
1
>>> get_calories(Vegetable('potato', 100))
100
>>> get_calories(Meat('steak', 500))
500

This ability to replace with a subclass is known as the “Liskov Substitution Principle”.

Now let’s turn to the consumers of food. Here is a possible class hierarchy:

class Omnivore:
    def eat(self, food):
        print(food.name, "YUM!")
class Vegetarian(Omnivore):
    def eat(self, food):
        if not isinstance(food, Vegetable):
            raise Exception(food.name + " EWW")
        super().eat(food)
class Carnivore(Omnivore):
    def eat(self, food):
        if not isinstance(food, Meat):
            raise Exception(food.name + " EWW")
        super().eat(food)

Vegetarian and Carnivore override the eat method of Omnivore to raise an exception when fed an argument that violates their respective dietary restrictions. An omnivore can consume both Vegetable and Meat:

>>> guest = Omnivore()
>>> guest.eat(Vegetable('potato'))
potato YUM!
>>> guest.eat(Meat('steak'))
steak YUM!

But a vegetarian cannot consume Meat:

>>> guest = Vegetarian()
>>> guest.eat(Vegetable('potato'))
potato YUM!
>>> guest.eat(Meat('steak'))
Traceback (most recent call last):
...
Exception: steak EWW

The code breaks when an Omnivore is replaced by a Vegetarian. Therefore, the Liskov Substitution Principle implies, perhaps counterintuitively, that Vegetarian is not a subclass of Omnivore.

This dilemma is resolved by observing that a Vegetarian can be replaced by an Omnivore, and hence the class hierarchy should be inverted - Omnivore being a subclass of Vegetarian. Of course, we can repeat the argument for Carnivore, so Omnivore inherits from both Vegetarian and Carnivore:

class Vegetarian:
    def eat(self, food):
        if not isinstance(food, Vegetable):
            return super().eat(food)
        print(food.name, "YUM!")
class Carnivore:
    def eat(self, food):
        if not isinstance(food, Meat):
            return super().eat(food)
        print(food.name, "YUM!")
class Omnivore(Vegetarian, Carnivore):
    pass

The key difference here is that, instead of raising an error, Vegetarian and Carnivore pass the call to eat up the inheritance chain, hoping that another class is able to accept the kind of food. The implementation of Omnivore is now trivial and intuitive - nothing more than the union of the consumers of Vegetable and Meat.

>>> guest = Vegetarian()
>>> guest.eat(Vegetable('potato'))
potato YUM!
>>> guest = Omnivore()
>>> guest.eat(Vegetable('potato'))
potato YUM!
>>> guest = Carnivore()
>>> guest.eat(Meat('steak'))
steak YUM!
>>> guest = Omnivore()
>>> guest.eat(Meat('steak'))
steak YUM!

We see that a Vegetarian or Carnivore can be replaced by an Omnivore, so our class hierarchy obeys Liskov Substitution. This example also illustrates a general rule: a method of the child class should accept an argument that is less restrictive (or not more restrictive) than the corresponding method of the parent class. An Omnivore can eat either Vegetable or Meat, whereas a Vegetarian or Carnivore can only eat one of the food classes. In other words, even though a child class is more specific than its parent, the child’s methods take arguments that are more general. This property of arguments is known as “contravariance”.