How to Write Cleaner Python Code Using Abstract Classes
What are Abstract Classes? Why are they useful? When should you use them? Let me give you a few examples and explanations! By the end of this post, you'll have a firm understanding of ABCs in Python, and how to add them to your programs.
Let's begin!
Our code without abstract classes
I believe the best way to learn is with an example, so let's take a look at the following code:
class Lion:
def give_food(self):
print("Feeding a lion with raw meat!")
class Panda:
def feed_animal(self):
print("Feeding a panda with some tasty bamboo!")
class Snake:
def feed_snake(self):
print("Feeding a snake with mice!")
# Animals of our zoo:
leo = Lion()
po = Panda()
sam = Snake()
Our job is to feed all the animals using a Python script. One way to do that would be:
leo.give_food()
po.feed_animal()
sam.feed_snake()
This would work. But imagine how much time would it take to do this for each animal in a large zoo, repeating the same process and code hundreds of times. That would also make the code harder to maintain.
Currently our program's structure looks something like this:
We want to optimize the process, so we could come up with a solution like this one:
# Put all the animals in a list:
zoo = [leo, po, sam] # Could be many more animals there!
# Loop through the animals and feed them
for animal in zoo:
# But what do we put here now?
# Is it animal.give_food() or animal.feed_animal(), hmm?
animal.feed() # This will throw an AttributeError!
The problem is that every class has a different method name, when feeding a lion it's give_food()
, when feeding a panda it's feed_animal()
and it's feed_snake()
for a snake.
This code is a mess because methods that do the same thing should be named the same.
If we could only force our classes to implement the same method names...
Introducing Abstract classes
It turns out that the Abstract class is what we need. Essentially it forces its subclasses to implement all of its abstract methods. It is a class that represents what its subclasses look like.
A better structure could look like like this (Animal
is an Abstract class):
By introducing an abstract class (Animal
), every class that inherits from Animal
must implement abstract methods from Animal
, which in our case is the method feed()
Let's take a look at the code:
from abc import ABC, abstractmethod
# abc is a builtin module, we have to import ABC and abstractmethod
class Animal(ABC): # Inherit from ABC(Abstract base class)
@abstractmethod # Decorator to define an abstract method
def feed(self):
pass
When defining an abstract class we need to inherit from the Abstract Base Class - ABC
.
To define an abstract method in the abstract class, we have to use a decorator: @abstractmethod
. The built-in abc
module contains both of these.
If you inherit from the Animal class but don't implement the abstract methods, you'll get an error:
class Panda(Animal): # If a class inherits from an ABC, it must implement all it's abstract methods!
def wrong_name(self): # The method's name must match the name of the ABC's method
print("Feeding a panda with some tasty bamboo!")
If we try to instantiate the class (e.g. po = Panda()
) it will throw a TypeError
since we can't instantiate Panda without an abstract method feed()
.
Keeping that in mind, we need to make our animals (this time correctly):
class Lion(Animal):
def feed(self):
print("Feeding a lion with raw meat!")
class Panda(Animal):
def feed(self):
print("Feeding a panda with some tasty bamboo!")
class Snake(Animal):
def feed(self):
print("Feeding a snake with mice!")
And lastly, this is all the code we need in order to create and feed our animals:
zoo = [Lion(), Panda(), Snake()]
for animal in zoo:
animal.feed() # Now this won't throw an error!
Writing abstract methods with parameters
What happens when an abstract method has parameters? When the subclass implements the method, it must contain all the parameters as well. The subclass' implementation can also add extra parameters if required.
from abc import ABC,abstractmethod
class Animal(ABC):
@abstractmethod
def do(self, action): # Renamed it to "do", and it has "action" parameter
pass
class Lion(Animal):
def do(self, action, time): # It's still mandatory to implement action. "time" is our other parameter
print(f"{action} a lion! At {time}")
class Panda(Animal):
def do(self, action, time):
print(f"{action} a panda! At {time}")
class Snake(Animal):
def do(self, action, time):
print(f"{action} a snake! At {time}")
zoo = [Lion(), Panda(), Snake()]
for animal in zoo:
animal.do(action="feeding", time="10:10 PM")
Running the above code will print out:
feeding a lion! At 10:10 PM
feeding a panda! At 10:10 PM
feeding a snake! At 10:10 PM
We could also use default arguments, you can read about those here.
Writing (abstract) properties
We may also want to create abstract properties and force our subclass to implement those properties. This could be done by using @property
decorator along with @absctractmethod
.
Since animals often have different diets, we'll need to define a diet
in our animal classes. Since all the animals are inheriting from Animal
, we can define diet
to be an abstract property. Besides diet
, we'll make food_eaten
property and it's setter
will check if we are trying to feed the animal something that's not on it's diet
.
Take a look at the code of Animal
, Lion
and Snake
:
from abc import ABC, abstractmethod
class Animal(ABC):
@property
def food_eaten(self):
return self._food
@food_eaten.setter
def food_eaten(self, food):
if food in self.diet:
self._food = food
else:
raise ValueError(f"You can't feed this animal with {food}.")
@property
@abstractmethod
def diet(self):
pass
@abstractmethod
def feed(self, time):
pass
class Lion(Animal):
@property
def diet(self):
return ["antelope", "cheetah", "buffaloe"]
def feed(self, time):
print(f"Feeding a lion with {self._food} meat! At {time}")
class Snake(Animal):
@property
def diet(self):
return ["frog", "rabbit"]
def feed(self, time):
print(f"Feeding a snake with {self._food} meat! At {time}")
We can create two objects, set the food that we're going to feed them and then call the feed()
method:
leo = Lion()
leo.food_eaten = "antelope"
leo.feed("10:10 AM")
adam = Snake()
adam.food_eaten = "frog"
adam.feed("10:20 AM")
That will print out:
Feeding a lion with antelope meat! At 10:10 AM
Feeding a snake with frog meat! At 10:10 AM
If we try to feed an animal something that it doesn't eat:
leo = Lion()
leo.food_eaten = "carrot"
leo.feed("10:10 AM")
The setter
will raise a ValueError
:
You can't feed this animal with carrot.
A word on abstract Meta class
You may have come across metaclasses when learning about abstract classes.
A class defines how an instance of the class behaves (e.g. Animal
describes how Lion
will behave). On the other hand a metaclass defines how a class behaves (ABCMeta
describes how every ABC
class will behave). A class is an instance of a metaclass.
The abc
module comes with a metaclass ABCMeta
. back in the days we had to use it to define metaclasses with metaclass=abc.ABCMeta
.
Nowadays, just inheriting from ABC
does the same thing—so you don't have to worry about metaclasses at all!
Summary
In this blog post we described the basics of Python's abstract classes. They are especially useful when working in a team with other developers and parts of the project are developed in parallel.
Here are some key takeaways:
- Abstract classes make sure that derived classes implement methods and properties defined in the abstract base class.
- Abstract base class can't be instantiated.
- We use
@abstractmethod
to define a method in the abstract base class and combination of@property
and@abstractmethod
in order to define an abstract property.
I hope you learnt something new today! If you're looking to upgrade your Python skills even further, check out our Complete Python Course.