4

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.

arne
  • 4,514
  • 1
  • 28
  • 47
  • You don't actually need classes to implement this in Python; functions are first-class objects and can be passed as arguments, rather than requiring an instance of an `Executor` class to carry the method. – chepner Mar 12 '20 at 13:43
  • @chepner This is true. My example simplifies quite a bit, in fact, I need the Executors to be classes because they hold some state. – arne Mar 12 '20 at 14:20

2 Answers2

2

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.

a_guest
  • 34,165
  • 12
  • 64
  • 118
  • Thank you for this solution! However, we use python 3.6, because we need to use RHEL, where a newer python is not (yet) natively available. – arne Mar 12 '20 at 12:16
  • 2
    @arne The `Protocol` class was backported to Python 3.[5-7] by the [`typing_extensions`](https://pypi.org/project/typing-extensions/) package. So you can also install that package. – a_guest Mar 12 '20 at 13:39
  • Do you know whether Sphinx understands `Protocol`s and can generate appropriate documentation on the `execute` function? – arne Mar 16 '20 at 09:55
  • @arne Do you mean whether it will link a class that implements a Protocol to the Protocol's doc string? I don't think so, because even if a class implements a Protocol, there's no guarantee that you intended this to be "derived" from the Protocol base. Just like with type annotations where you explicitly require an object of type `Executor` (and hence the type checker can work out whether they are compatible), you would need to declare any doc string inheritance explicitly too. But you could use a class decorator which updates doc strings from the Protocol class. – a_guest Mar 16 '20 at 10:44
2

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.

Jesko Hüttenhain
  • 1,278
  • 10
  • 28