3

I have an abstract class written in Python:

class AbsctracClass(ABC):

    @abstractmethod
    def method(self, 
        value1: int,
        value2: float,
        value3: str,
        value4: Optional[list] = None,
        value5: Optional[int] = None,
        value6: Optional[float] = None,
        ... etc ...,
        ):
        ''' An abstract method  for an abstract class '''

It has one abstract method with a lot of arguments. I know, I know, it is not a good practice to pass lots of arguments to a function, but right now it's irrelevant for my question.

Now I want to write another class that inherits from AbstractClass. And I have to manually duplicate all the arguments with their type hints from an abstract class.

class AnotherClass(AbstractClass):

    def method(self, 
        value1: int,
        value2: float,
        value3: str,
        value4: Optional[list] = None,
        value5: Optional[int] = None,
        value6: Optional[float] = None,
        ... etc ...,
        ):
        return value3 * (value2 + value3)  

It is not only cumbersome when I have several abstract methods and several classes inherits from it, but pylint screams about code duplication. And, to be honest, I agree with him.

Obviously there must be a better way. Is it?

  • 2
    Doing this manually is better in the long run. Otherwise, use `*args, **kwargs`. – juanpa.arrivillaga Jun 21 '21 at 04:24
  • 1
    Why is it better in the long run? What if I had to change type hint for some of arguments? I had to refactor all inherited classes then? And pylint screams at *args and **kwargs too when they are inside abstract method inhereted from abstract class. – Mihail Kondratyev Jun 21 '21 at 04:38
  • 1
    You can't change the type hints without violating the Liskov Substitution Principle. The *signature* is the important thing that the ABC dictates. – chepner Jul 24 '21 at 16:59
  • 1
    Just because the ABC cannot *enforce* the preservation of the signature does not mean you should feel free to change it. – chepner Jul 24 '21 at 17:02
  • 1
    But I don't want to change type hints. I just feel it redundant to blindly repeat them all each time I inherit ABC. – Mihail Kondratyev Aug 05 '21 at 23:39

1 Answers1

2

One solution here could be to use a single NamedTuple argument, in both the abstract class and the subclasses. If we have a NamedTuple defined like so:

from typing import NamedTuple, Optional 

class ArgsTuple(NamedTuple):
    value1: int
    value2: float
    value3: str
    value4: Optional[list] = None
    value5: Optional[int] = None
    value6: Optional[float] = None

Then you can refactor your base class like this:

from abc import ABC, abstractmethod

# ArgsTuple as defined above

class AbstractClass(ABC):
    @abstractmethod
    def method(self, all_args: ArgsTuple):
        ''' An abstract method  for an abstract class '''

Your concrete implementations also have to change their signatures for method, to keep the signature the same between the base class and the subclass. The way the function is called will also need to change — method now only takes one argument, so you will have to wrap all the arguments into ArgsTuple before passing them to method.

In terms of implementing method, you'll now have to unpack the arguments from the ArgsTuple within the method. One way of doing this is like this, which some static type-checkers might struggle with (MyPy can infer the types, but if I remember correctly, PyCharm gets lost if you use tuple unpacking):

# AbstractClass as refactored above
# ArgsTuple as defined above 

class AnotherClass(AbstractClass):
    def method(self, all_args: ArgsTuple):
        _, value2, value3, *other_args = all_args
        return value3 * (value2 + value3)

Another way of doing it would be like this, which some static type-checkers may find easier to handle:

# AbstractClass as refactored above
# ArgsTuple as defined above 

class AnotherClass(AbstractClass):
    def method(self, all_args: ArgsTuple):
        value3 = all_args.value3
        return value3 * (all_args.value2 + value3)
Alex Waygood
  • 6,304
  • 3
  • 24
  • 46