2

I'm dealing with two categories of machine learning algorithms. For simplicity, let's call them A and B.

There are multiple concrete algorithms in each category, and my goal is to implement all of them. Because of this, my initial plan was to define abstract classes CategoryAAlgorithms and CategoryBAlgorithms, and have each of them define a set of methods that must be implemented by the concrete algorithms. So far so good - these abstract classes serve as a nice template and I find that they make my project a lot more understandable.

However, after I've done this, I found that the abstract methods defined in CategoryBAlgorithms contain all the abstract methods defined in CategoryAAlgorithms.

To avoid code duplication, I want to have CategoryBAlgorithms inherit from CategoryAAlgorithms. However, somewhere else in my code I do the following checks:

# just an example
if isinstance(some_instance_of_a_concrete_algorithm, CategoryAAlgorithms):
    do_some_kind_of_preprocessing()
elif isinstance(some_instance_of_a_concrete_algorithm, CategoryBAlgorithms):
    do_some_other_kind_of_preprocessing()

This means that, if I do the inheritance, then for a concrete algorithm from category B, both if statements would be true.

Is there a way for me to get around this issue? My initial thought was to check for the closest abstract parent, but I don't think this is a clean solution, so I welcome other ideas.

Thanks in advance.

2
  • Mixing object-oriented and functional paradigms is a big NO (aside very few very specific exceptions). If you want to stay OOP, then the pre-processing step must be part of some object (not necessarily the algorithm itself), and you should be asking how to make it a part of an object and what kind of object. Otherwise, the question is contradicting (because it's tagged OOP but asking for non-OOP solution) and likely is not answerable. Commented Jun 4, 2021 at 4:05
  • 2
    Why can't you simply reverse the checks and rely on the elif? In real life it will probably be a lot more obvious than in your simplified nomenclature that B algorithms are the more specific ones, so it won't be a big burden to remember to check for B first. Commented Jun 4, 2021 at 6:47

1 Answer 1

2

Object-oriented approaches are legitimate. Non-OOP approaches are legitimate. But mixing them like this does not seem wise.

You have noticed that all CategoryB objects also have all methods of CategoryA objects. But does this mean that that CategoryB is a subclass of CategoryA? If it is a subclass, then wherever your code expects a CategoryA instance you can provide a CategoryB object instead, and everything should work. This is also known as the “Liskov Substitution Principle” (LSP).

Yet, you find that this doesn't hold – your CategoryB objects need to be used differently. They require different preprocessing. To me, this indicates that you should not use inheritance here.

Sometimes, things look related but aren't. Principles like Don't Repeat Yourself (DRY) are rough guidelines. But often, coupling things that aren't related is worse than a bit of repetition.

Of course, it can reasonably happen that two classes happen to share the same implementation, but that they shouldn't be subclasses of each other in the LSP sense. Then, instead of using inheritance class CategoryB(CategoryA), it could be better to just call a common function:

class CategoryA:
  def some_function(self, x):
    return _some_shared_function(x, self.a)

class CategoryB:
  def some_function(self, x):
    return _some_shared_function(x, self.b)

def _some_shared_function(x, y):
  ...

So far, I have assumed that CategoryA and CategoryB should not be related. But what if your subclassing is actually legitimate? Then the object in question should provide all type-specific operations that are necessary. You should tell the object what to do, and the object will know how to do that. You shouldn't ask the object for its specific type. Here, you might need a method for preprocessing:

class CategoryA:
  def preprocess(self):
    do_some_kind_of_preprocessing()

class CategoryB(CategoryA):
  def preprocess(self):
    do_some_other_kind_of_preprocessing()

Now, instead of having to remember a specific order of instanceof() tests, the inheritance resolves this.

The drawback of this approach is that your objects will accumulate a lot of behaviour, potentially leading to a “god object” with a large, ill-defined public interface. If this becomes unmanageable, the Visitor Pattern may provide an alternative. The Visitor allows you to define type-dependent operations externally, and still let the object select its correct implementation. Here, it would look like this:

class CategoryA:
  def accept_visitor(self, visitor, *args, **kwargs):
    return visitor.visit_category_a(self, *args, **kwargs)

class CategoryB:
  def accept_visitor(self, visitor, *args, **kwargs):
    return visitor.visit_category_b(self, *args, **kwargs)

class PreprocessingVisitor:
  def visit_category_a(visitor, self):
   do_some_kind_of_preprocessing()

  def visit_category_b(visitor, self):
   do_some_other_kind_of_preprocessing() 

some_algorithm_instance.accept_visitor(PreprocessingVisitor())
2
  • I think the LSP principle was really enlightening. I'm a research student so my background in software design is limited. Mind if I ask, could you use functions that are not part of any class in OOP? Commented Jun 6, 2021 at 3:44
  • @Wiza While the LSP was popularized in industry as part of the “SOLID principles”, it actually comes from academia as well! Regarding mixing OOP and non-OOP approaches, my first paragraph seems to discourage this, but that's because of the “Tell, Don't Ask” violation explained later. There is no problem with mixing classes and free functions. So yes, do just use functions if that is helpful. Don't stuff code into a class unless it needs to be part of that object.
    – amon
    Commented Jun 6, 2021 at 12:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.