One could also do a raise NotImplementedError()
inside the child method of an @abstractmethod
-decorated base class method.
Imagine writing a control script for a family of electronic measurement modules (i.e. physical devices).
The functionality of each module is narrowly-defined, implementing just one dedicated function:
one could be an array of relays, another a multi-channel DAC or ADC, another an ammeter etc.
Many of the low-level commands would be shared between the modules for example to read
their ID numbers or to send a command to them. Let's see what we have at this point:
Base Class
from abc import ABC, abstractmethod #< we'll make use of these later
class Generic(ABC):
''' Base class for all measurement modules. '''
# Shared functions
def __init__(self):
# do what you must...
def _read_ID(self):
# same for all the modules
def _send_command(self, value):
# same for all the modules
Shared Verbs
We then realise that much of the module-specific command verbs and, therefore, the
logic of their interfaces is also shared. Here are 3 different verbs whose meaning
would be self-explanatory considering a number of target modules.
get(channel)
:
- relay: get the on/off status of the relay on
channel
- DAC: get the output voltage on
channel
- ADC: get the input voltage on
channel
enable(channel)
:
- relay: enable the use of the relay on
channel
- DAC: enable the use of the output channel on
channel
- ADC: enable the use of the input channel on
channel
set(channel)
:
- relay: set the relay on
channel
on/off
- DAC: set the output voltage on
channel
- ADC: hmm... nothing logical comes to mind.
Shared Verbs Become Enforced Verbs
I'd argue that there is a strong case for the above verbs to be shared across the modules
as we saw that their meaning is evident for each one of them. I'd continue writing my
base class Generic
like so:
class Generic(ABC): # ...continued
@abstractmethod
def get(self, channel):
pass
@abstractmethod
def enable(self, channel):
pass
@abstractmethod
def set(self, channel):
pass
Subclasses
We now know that our subclasses will all have to define these methods. Let's see what it
could look like for the ADC module:
class ADC(Generic):
def __init__(self):
super().__init__() #< applies to all modules
# more init code specific to the ADC module
def get(self, channel):
# returns the input voltage measured on the given 'channel'
def enable(self, channel):
# enables accessing the given 'channel'
You may now be wondering:
But this won't work for the ADC module as set
makes no sense there as we've just seen this above!
You're right: not implementing set
is not an option as Python would then fire the error below
when you tried to instantiate your ADC object.
TypeError: Can't instantiate abstract class 'ADC' with abstract methods 'set'
So you must implement something, because we made set
an enforced verb (aka '@abstractmethod'),
which is shared by two other modules but, at the same time, you must also not implement anything as
set
does not make sense for this particular module.
NotImplementedError to the Rescue
By completing the ADC class like this:
class ADC(Generic): # ...continued
def set(self, channel):
raise NotImplementedError("Can't use 'set' on an ADC!")
You are doing three very good things at once:
- You are protecting a user from erroneously issuing a command ('set') that is
not (and shouldn't!) be implemented for this module.
- You are telling them explicitly what the problem is (see TemporalWolf's link about
'Bare exceptions' for why this is important)
- You are protecting the implementation of all the other modules for which the enforced verbs
do make sense. I.e. you ensure that those modules for which these verbs do make sense will
implement these methods and that they will do so using exactly these verbs and not some
other ad-hoc names.