Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Checking of **kwargs in concrete implementation of abstract class method. Interface issue?

I am trying to implement the Strategy design pattern to create an interface for an underlying algorithm to be implemented in a modular fashion.

Currently, as per code below, I have one top-level/parent abstract class (ParentAbstractStrategy) that defines the base interface for the strategy method.

I also have a one-level-down from this abstract class (ChildAbstractStrategy).

The reason I have two abstract classes is because of the attributes they need to hold; see the __init__ methods. ChildAbstractStrategy is a special case of ParentAbstractStrategy in that it stores an additional attribute: attr2. Otherwise its interface is identical, as seen by identical strategy method signatures.

Sometimes, I want to be able to directly subclass ParentAbstractStrategy and implement the strategy method (see ConcreteStrategyA), but other times I want to be able to subclass ChildAbstractStrategy, because the extra attribute is required (see ConcreteStrategyB).

An additional complication is that in some subclasses of either abstract class I want to be able to handle additional arguments in the strategy method. This is why I have added **kwargs to all signatures of the strategy method, so that I can pass in whatever additional arguments I want to a subclass, on a case-by-case basis.

This creates the last problem: these extra arguments are not optional in the subclasses. E.g. in the strategy method of ConcreteStrategyB I want to be certain that the caller passed in a third argument. I'm basically abusing **kwargs to provide what probably should be positional arguments (since I can't give them sane defaults and need their existence to be enforced).

This current solution of using **kwargs for "method overloading" in subclasses feels really messy, and I'm not sure if this means there is a problem with the class inheritance scheme or interface design, or both.

Is there a way that I can achieve these design goals in a cleaner fashion. It feels like I'm missing something big picture here and maybe the class/interface design is bad. Maybe creating two disjoint abstract classes with different signatures for the strategy method?

import abc


class ParentAbstractStrategy(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def __init__(self, attr1):
        self.attr1 = attr1

    @abc.abstractmethod
    def strategy(self, arg1, arg2, **kwargs):
        raise NotImplementedError


class ChildAbstractStrategy(ParentAbstractStrategy, metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def __init__(self, attr1, attr2):
        super().__init__(attr1)
        self.attr2 = attr2

    @abc.abstractmethod
    def strategy(self, arg1, arg2, **kwargs):
        raise NotImplementedError


class ConcreteStrategyA(ParentAbstractStrategy):
    def __init__(self, attr1):
        super().__init__(attr1)

    def strategy(self, arg1, arg2, **kwargs):
        print(arg1, arg2)


class ConcreteStrategyB(ChildAbstractStrategy):
    def __init__(self, attr1, attr2):
        super().__init__(attr1, attr2)

    def strategy(self, arg1, arg2, **kwargs):
        print(arg1, arg2)
        arg3 = kwargs.get("arg3", None)

        if arg3 is None:
            raise ValueError("Missing arg3")
        else:
            print(arg3)

Here's an interpreter session demonstrating how it's currently working:

>>> a = ConcreteStrategyA(1)
>>> a.attr1
1
>>> a.strategy("a", "b")
a b
>>> b = ConcreteStrategyB(1, 2)
>>> b.attr1
1
>>> b.attr2
2
>>> b.strategy("a", "b")
a b
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/space/strategy.py", line 42, in strategy
    raise ValueError("Missing arg3")
ValueError: Missing arg3
>>> b.strategy("a", "b", arg3="c")
a b
c
like image 915
Space Avatar asked Aug 02 '19 06:08

Space


People also ask

Can an abstract class have concrete methods?

Abstract class can have both an abstract as well as concrete methods. A concrete class can only have concrete methods. Even a single abstract method makes the class abstract.

Can an abstract class implement an interface?

Java Abstract class can implement interfaces without even providing the implementation of interface methods. Java Abstract class is used to provide common method implementation to all the subclasses or to provide default implementation.

Can abstract method have parameters in Python?

The semantics of an abstract method almost always include the parameters it should take.

What is an abstract class in Java?

Abstract class: is a restricted class that cannot be used to create objects (to access it, it must be inherited from another class). Abstract method: can only be used in an abstract class, and it does not have a body. The body is provided by the subclass (inherited from).

Can an abstract class implement all methods in an interface?

In Java, an abstract class can implement an interface, and not provide implementations of all of the interface’s methods. It is the responsibility of the first concrete class that has that abstract class as an ancestor to implement all of the methods in the interface.

What is the difference between abstract class and concrete class?

Unlike an interface or abstract class, a concrete class can be instantiated. It demonstrates the implementation of a blueprint. Any abstract methods are overridden, to include a method body. A concrete class can implement multiple interfaces, but can only inherit from one parent class.

Can a concrete class implement multiple interfaces in Java?

A concrete class can implement multiple interfaces, but can only inherit from one parent class. In the example below, the Dog class inherits its methods from the interface, Pet, and the abstract class, Animal.

Who is responsible for implementing all the methods of an interface?

It is the responsibility of the first concrete class that has that abstract class as an ancestor to implement all of the methods in the interface. C# on the other hand seems to require that the abstract class provide implementations for all of the methods on the interface.


Video Answer


1 Answers

Answering my own question.

My usage of **kwargs is 'bad' in this scenario. Why? As far as I can tell, **kwargs is typically used for:

  1. Wrapper functions, e.g. decorators.
  2. Collecting extra keyword arguments to a function that the function knows about (e.g. see usage in https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html?highlight=plot#matplotlib.pyplot.plot). In this scenario the **kwargs are optional arguments that can be passed into the function and they have sane default values.

Making **kwargs to be required in a function call is defeating their purpose; positional arguments that need to be explicitly supplied should be used instead. That way the interface provided by the function has to be explicitly satisfied by the caller.

There is another problem with using **kwargs in an interface, as I have. It involves the LSP (Liskov Substitution Principle, see https://en.wikipedia.org/wiki/Liskov_substitution_principle). The current implementation is abusing **kwargs in an attempt to define a variable interface for the strategy method among sublcasses. Although syntactically the function signatures for all strategy methods match, semantically the interfaces are different. This is a violation of the LSP, which would require that I could e.g. treat any descendant of ParentAbstractStrategy the same when considering their interfaces, e.g. I should be able to treat the strategy method of ConcreteStrategyA and ConcreteStrategyB the same.

What was my solution? I have changed the interface of the strategy method to no longer include **kwargs and instead use a mix of positional arguments and keyword arguments with default values. E.g. if ConcreteStrategyB still needs a third argument arg3 but ConcreteStrategyA does not, I could change the classes to look like this:

class ConcreteStrategyA(ParentAbstractStrategy):
    def __init__(self, attr1):
        super().__init__(attr1)

    def strategy(self, arg1, arg2, arg3=None):
        print(arg1, arg2)


class ConcreteStrategyB(ChildAbstractStrategy):
    def __init__(self, attr1, attr2):
        super().__init__(attr1, attr2)

    def strategy(self, arg1, arg2, arg3=None):
        print(arg1, arg2)
        assert arg3 is not None
        print(arg3)

With the interfaces of both parent classes changed to match.

like image 108
Space Avatar answered Oct 18 '22 01:10

Space