-1

I came accross a piece of Python legacy code at work that I couldn't understand how it could work without errors. Obviously I can't write the exact code here but here is a minimal working example:

class ClassB:
    def func(self, txt: str):
        return self.str_to_uppercase(txt)


class ClassA(ClassB):
    def str_to_uppercase(self, txt: str):
        return txt.upper()


if __name__ == "__main__":
    my_instance = ClassA()
    print(my_instance.func("Hello, World!"))

stdout: HELLO, WORLD!

What's strange to me is that, although ClassB is not directly inheriting from ClassA where the instance method str_to_uppercase() is defined, ClassB is still able to call this method. I should also note that my linter (pylint) is complaining that str_to_uppercase() is not defined in ClassB. So I'm struggling to understand how the mechanics of the code works here regarding inheritence.

Secondly, this code looks strange to me. It doesn't seem very "Pythonic". So, as a second question, I was wondering in which usecases such code is useful?

glpsx
  • 587
  • 1
  • 7
  • 21
  • 1
    `my_instance` is an instance of `ClassB` as much as it is an instance of `ClassA`, precisely because of inheritance. It's not clear what you are confused about. `str_to_uppercase` doesn't even *care* what `self` is, because it doesn't use it. – chepner Jan 23 '23 at 17:30
  • 1
    "although ClassB is not directly inheriting from ClassA where the instance method str_to_uppercase() is defined, ClassB is still able to call this method." I can't understand your reasoning. Why should that be a limitation? – Karl Knechtel Jan 23 '23 at 17:31
  • `str_to_uppercase` can be a static method, or it could be a regular function without `ClassA` being defined at all. The unpythonic part is where `ClassB.func` depends on a method that isn't defined by `ClassB` at all. – chepner Jan 23 '23 at 17:32
  • Does this answer your question? [What is a mixin and why is it useful?](https://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-is-it-useful) – Abdul Aziz Barkat Jan 23 '23 at 17:32
  • 2
    OP is confused that `ClassB` does not have a `str_to_uppercase` method but has code where `self.str_to_uppercase` is used. Python is a dynamically typed language so this is completely valid. If you created an instance of `ClassB` and called `func` you would actually get an error like you expect to. – Abdul Aziz Barkat Jan 23 '23 at 17:34
  • 1
    @chepner Mixins are quite common in Python though? How is that unpythonic? – Abdul Aziz Barkat Jan 23 '23 at 17:35
  • Because a subclass of `ClassB` can fail to define `str_to_uppercase`, resulting in a instance for which `func` won't work. It would be fine if `ClassB` provided a method that `ClassA` made use of. This (`ClassB` expecting all its subclasses to provide a method *it* needs) is backwards. – chepner Jan 23 '23 at 17:43
  • At the very least, `ClassB` should provide an empty definition of `str_to_uppercase` so that `func` doesn't fail with an `AttributeError` whether or not a subclass overrides it. – chepner Jan 23 '23 at 17:44
  • @chepner generally these mixins aren't meant to be used alone, the area they begin shining in is when you have multiple inheritance. The idea is you inherit from a parent class which does more of the heavy lifting **and** you inherit from the mixin which adds more functionality. This pattern is quite widely used in frameworks like Django, etc. – Abdul Aziz Barkat Jan 23 '23 at 17:47
  • 1
    I don't consider `ClassB` a mix-in at all. It's not providing any functionality to `ClassA`, rather `ClassA` is providing functionality to `ClassB`. (Maybe you could call it a contravariant mix-in; I call it a design error.) – chepner Jan 23 '23 at 17:48

2 Answers2

0

This is quite standard in OO and even has a name: The Template Method Pattern.

The idea is that ClassB should define some useful functionality, but only at a high level. Its code calls other methods that it doesn't define, but still depends on.

This leaves other classes (ClassA in your example) to provide the fine details, called implementation details.

quamrana
  • 37,849
  • 12
  • 53
  • 71
0

When a method is called on an object, Python accesses the list of defined methods of the actual class of the object. Inside the ClassB.func(self,...) method, the actual class of the self object is ClassA. ClassA has a str_to_uppercase() so it can be called even though this code is part of ClassB (which doesn't need to know ClassA).

If you passed my_instance to any regular function that expects an object as parameter, you would be able to call the str_to_uppercase() method of that object within the function (and that would not be a surprise). The name self in method definitions is actually just a convention for the first parameter of the function which is implicitly the instance of the object calling it.

Alain T.
  • 40,517
  • 4
  • 31
  • 51