Python is considered to be a very flexible programming language, but that doesn’t mean that there are no controls to impose a set of functionalities or an order in a class hierarchy. When you develop a system in a group of programmers, it would be useful to have some means of establishing requirements for classes in matters of interfaces (methods) exposed by each class.
An abstract class should be considered a blueprint for other classes, a kind of contract between a class designer and a programmer:
This contract assures you that a child class, built upon your abstract class, will be equipped with a set of concrete methods imposed by the abstract class.
The very important reason is: we want our code to be polymorphic, so all subclasses have to deliver a set of their own method implementations in order to call them by using common method names.
Furthermore, a class which contains one or more abstract methods is called an abstract class. This means that abstract classes are not limited to containing only abstract methods – some of the methods can already be defined, but if any of the methods is an abstract one, then the class becomes abstract.
An abstract method is a method that has a declaration, but does not have any implementation. We'll give some examples of such methods to emphasize their abstract nature.
Let's talk about an example:
Assume that you’re designing a music player application, intended to support multiple file formats. Some of the formats are known now, but some are not yet known. The idea is to design an abstract class representing a base music format and corresponding methods for “open”, “play”, “get details”, “rewind”, etc., to maintain polymorphism.
Your team should implement concrete classes for each format you'd like to support. Whenever new format specifications become available, you won’t have to rework your music player code, you’ll just have to deliver a class supporting the new file format, fulfilling the contract imposed by the abstract class.
Remember that it isn’t possible to instantiate an abstract class, and it needs subclasses to provide implementations for those abstract methods which are declared in the abstract classes. This behavior is a test performed by a dedicated Python module to validate if the developer has implemented a subclass that overrides all abstract methods.
When we’re designing large functional units, in the form of classes, we should use an abstract class. When we want to provide common implemented functionality for all implementations of the class, we could also use an abstract class, because abstract classes partially allow us to implement classes by delivering concrete definitions for some of the methods, not only declarations.
We have just defined the means by which to provide a common Application Program Interface (API) for a set of subclasses. This capability is especially useful in situations where your team or third-party is going to provide implementations, such as with plugins in an application, even after the main application development is finished.
Let's start with a typical class that can be instantiated:
class BluePrint: def hello(self): print('Nothing is blue unless you need it') bp = BluePrint() bp.hello()
There’s nothing new for you here – it’s just an example of creating a class and instantiating it. No errors, no doubts, and it gives a clear output:
Nothing is blue unless you need it
Python has come up with a module which provides the helper class for defining Abstract Base Classes (ABC) and that module name is abc.
The ABC allows you to mark classes as abstract ones and distinguish which methods of the base abstract class are abstract. A method becomes abstract by being decorated with an @abstractmethod decorator.
To start with ABC you should:
abc module;ABC, which is delivered by the abc module;@abstractmethod, which is delivered by the abc module.import abc class BluePrint(abc.ABC): @abc.abstractmethod def hello(self): pass class GreenField(BluePrint): def hello(self): print('Welcome to Green Field!') gf = GreenField() gf.hello()
Welcome to Green Field!
So far everything works fine, so let's play with the code a little.
Now we'll try to instantiate the BluePrint class:
import abc class BluePrint(abc.ABC): @abc.abstractmethod def hello(self): pass class GreenField(BluePrint): def hello(self): print('Welcome to Green Field!') gf = GreenField() gf.hello() bp = BluePrint()
The output:
Welcome to Green Field!
Traceback (most recent call last):
(...)
bp = BluePrint()
TypeError: Can't instantiate abstract class BluePrint with abstract methods hello
indicates that:
GreenField class and call the hello method, because the Python developer has provided a concrete definition of the hello method. In other words, the Python developer has overridden the abstract method hello with their own implementation. When the base class provides more abstract methods, all of them must be overridden in a subclass before the subclass can be instantiated.TypeError exception when we try to instantiate the base BluePrint class, because it contains an abstract method.
Now we'll try to inherit the abstract class and forget about overriding the abstract method by creating a RedField class that does not override the hello method.
import abc class BluePrint(abc.ABC): @abc.abstractmethod def hello(self): pass class GreenField(BluePrint): def hello(self): print('Welcome to Green Field!') class RedField(BluePrint): def yellow(self): pass gf = GreenField() gf.hello() rf = RedField()
The output:
Welcome to Green Field!
Traceback (most recent call last):
(...)
rf = RedField()
TypeError: Can't instantiate abstract class RedField with abstract methods hello
indicates that:
GreenField class and call the hello method;RedField class is still recognized as an abstract one, because it inherits all elements of its super class, which is abstract, and the RedField class does not override the abstract hello method.Multiple inheritance When you plan to implement a multiple inheritance from abstract classes, remember that an effective subclass should override all abstract methods inherited from its super classes.
Summary:
A note:
It’s tempting to call a module “abc” and then try to import it, but by doing so Python imports the module containing the ABC class instead of your local file. This could cause some confusion – why does such a common name as “abc” conflict with my simple module “abc”?
Run your own experiment to become familiar with the error messages you would encounter in such a situation.
#!/usr/bin/env python # -*- coding: utf-8 -* import abc class Scanner(abc.ABC): def __init__(self): self.scanner_resolution = 100 @abc.abstractmethod def scan_document(self) -> str: pass @abc.abstractmethod def get_scanner_status(self) -> str: pass class Printer(abc.ABC): def __init__(self): self.printer_resolution = 100 self.printer_operation_history = False self.fax_machine = False @abc.abstractmethod def print_document(self) -> str: pass @abc.abstractmethod def get_printer_status(self) -> str: pass class MFD(): def __init__(self,serial_number=""): self.serial_number = serial_number class MFD1(MFD,Scanner,Printer): def __init__(self,serial_number=""): MFD.__init__(self,serial_number) Scanner.__init__(self) Printer.__init__(self) def low(self): self.scanner_resolution = 100 self.printer_resolution = 100 self.printer_operation_history = False self.fax_machine = False def medium(self): self.scanner_resolution = 300 self.printer_resolution = 300 self.printer_operation_history = True self.fax_machine = False def high(self): self.scanner_resolution = 600 self.printer_resolution = 600 self.printer_operation_history = True self.fax_machine = True def scan_document(self): print("El document ha estat escanejat") def get_scanner_status(self): fax = "" if self.fax_machine == False else ", Fax" print( "màxima resolució scanner: {}{} número de sèrie:{}".format(self.scanner_resolution,fax,self.serial_number) ) def print_document(self): print("El document ha estat imprés") def get_printer_status(self): poh = "" if self.printer_operation_history == False else ", Històric d'impressora" print( "màxima resolució impressora: {}{} número de sèrie:{}".format(self.scanner_resolution,poh,self.serial_number) ) d1 = MFD1("d1-sn") d1.low() d1.get_scanner_status() d1.get_printer_status() d2 = MFD1("d2-sn") d2.medium() d2.get_scanner_status() d2.get_printer_status() d3 = MFD1("d3-sn") d3.high() d3.get_scanner_status() d3.get_printer_status()
#!/usr/bin/env python # -*- coding: utf-8 -* import abc class Scanner(abc.ABC): def __init__(self): self.scanner_resolution = 100 @abc.abstractmethod def scan_document(self) -> str: pass @abc.abstractmethod def get_scanner_status(self) -> str: pass class Printer(abc.ABC): def __init__(self): self.printer_resolution = 100 self.printer_operation_history = False self.fax_machine = False @abc.abstractmethod def print_document(self) -> str: pass @abc.abstractmethod def get_printer_status(self) -> str: pass class MFD(): def __init__(self,serial_number=""): self.serial_number = serial_number def scan_document(self): print("El document ha estat escanejat") def get_scanner_status(self): fax = "" if self.fax_machine == False else ", Fax" print( "màxima resolució scanner: {}{} número de sèrie:{}".format(self.scanner_resolution,fax,self.serial_number) ) def print_document(self): print("El document ha estat imprés") def get_printer_status(self): poh = "" if self.printer_operation_history == False else ", Històric d'impressora" print( "màxima resolució impressora: {}{} número de sèrie:{}".format(self.scanner_resolution,poh,self.serial_number) ) class MFD1(MFD,Scanner,Printer): def __init__(self,serial_number=""): MFD.__init__(self,serial_number) Scanner.__init__(self) Printer.__init__(self) self.low() def low(self): self.scanner_resolution = 100 self.printer_resolution = 100 self.printer_operation_history = False self.fax_machine = False class MFD2(MFD,Scanner,Printer): def __init__(self,serial_number=""): MFD.__init__(self,serial_number) Scanner.__init__(self) Printer.__init__(self) self.medium() def medium(self): self.scanner_resolution = 300 self.printer_resolution = 300 self.printer_operation_history = True self.fax_machine = False class MFD3(MFD,Scanner,Printer): def __init__(self,serial_number=""): MFD.__init__(self,serial_number) Scanner.__init__(self) Printer.__init__(self) self.high() def high(self): self.scanner_resolution = 600 self.printer_resolution = 600 self.printer_operation_history = True self.fax_machine = True d1 = MFD1("d1-sn") d1.get_scanner_status() d1.get_printer_status() d2 = MFD2("d2-sn") d2.get_scanner_status() d2.get_printer_status() d3 = MFD3("d3-sn") d3.get_scanner_status() d3.get_printer_status()
màxima resolució scanner: 100 número de sèrie:d1-sn màxima resolució impressora: 100 número de sèrie:d1-sn màxima resolució scanner: 300 número de sèrie:d2-sn màxima resolució impressora: 300, Històric d'impressora número de sèrie:d2-sn màxima resolució scanner: 600, Fax número de sèrie:d3-sn màxima resolució impressora: 600, Històric d'impressora número de sèrie:d3-sn