Using abstract base class for documentation purposes only

Viewed 851

I have a suite of similar classes called 'Executors', which are used in the Strategy pattern. They are very simple classes:

class ExecutorA:
  def execute(self, data):
    pass

class ExecutorB:
  def execute(self, data):
    pass

The execute() functions of all executors need to behave in the same way, i.e. take data in a certain format and return data in another certain format. Due to duck-typing, there is no need to have a base class, so I didn't write one.

However, I am currently documenting my code using docstrings etc. and I thought it could be a good idea to document the data format requirements of the execute() functions in an abstract base class like so:

class Executor(ABC):
  def execute(self, data):
  """Process data in format xy and return data in format zy"""
    pass

My rationale is that I don't want to replicate the same documentation in every Executor class. Is this a common use case for ABC's?

Clarification: I need to use python 3.6 because we are using RHEL and a newer python is not yet officially available.

2 Answers

If it's for documentation / static type checking purposes only you can also use typing.Protocol (starting with Python 3.8 and backported via typing_extensions). This is used for structural subtyping which doesn't require explicit inheritance. So you could do:

from typing import List, Protocol

class Executor(Protocol):
    def execute(self, data: List[float]) -> float:  # example type annotations
        """Reduce `data` to a single number."""
        ...

class ExecutorA:  # no base class required, it implements the protocol
    def execute(self, data: List[float]) -> float:
        return sum(data)

def do_work(worker: Executor,  # here we can use the Protocol class
            data: List[float]) -> float:
    return worker.execute(data)

do_work(ExecutorA(), [1., 2., 3.])  # this check passes

The doc string here is on the protocol class providing general information what the execute method does. Since Executor is used for type annotations users will be referred to the protocol class. If desired, additional information can be added to the implementations (ExecutorA, ...) as well, or the original doc string can be copied (this work can be done by a decorator).

The usage of abstract base classes is also a solution. ABCs allow for isinstance and issubclass checks and you can register additional classes that don't inherit the ABC explicitly.

For what it's worth, I think that using an abstract base class for this is a reasonable option in Python 3.6. You may want to decorate execute as an @abstractmethod, but ¯\_(ツ)_/¯.

Now if you want a little more control over your docstrings, you could make your own metaclass that inherits from ABCMeta. For example, the following is a way to have "extensible" docstrings in the sense that your docstrings become format strings where {pdoc} will always be replaced by the documentation of your (first) parent class (if it exists).

from abc import abstractmethod, ABCMeta
from inspect import getdoc

class DocExtender(ABCMeta):
    def __new__(cls, name, bases, spec):
        for key, value in spec.items():
            doc = getattr(value, '__doc__', '{pdoc}')
            try:
                pdoc = getdoc(getattr(bases[0], key))
            except (IndexError, AttributeError):
                pdoc = ''
            try:
                value.__doc__ = doc.format(pdoc=pdoc)
            except AttributeError:
                pass
        return ABCMeta.__new__(cls, name, bases, spec)

class ExecutorBase(metaclass=DocExtender):
    @abstractmethod
    def execute(self, data):
        """
        Parent
        """
        pass

class Executor1(ExecutorBase):
    def execute(self, data):
        """
        {pdoc} - Child
        """
        return sum(data)

class Executor2(ExecutorBase):
    def execute(self, data):
        return sum(data)

print(getdoc(Executor1.execute))
# Parent - Child

print(getdoc(Executor2.execute))
# Parent

I am posting this mainly to illustrate the general concept; adjust as needed, obviously.

Related