Is super() super for me?

So I just watched Hettinger’s video about super. And I think I got the idea.

Though I’m not sure when to apply it. On stackeroverflow I found a question where super is mentioned as a pythonic way of doing dependency injection.

So I thought, hey, I’d use it in one of my projects, where I have a data analysis script which gets data from different data sources. In C# I would implement an interface for the datasource provider so that it is exchangable in the data analyser script.

class DataProviderInterface():
    def get_cool_data(self):
        raise NotImplementedError()

class DataProvider(DataProviderInterface):
    def get_cool_data(self):
        return super().get_cool_data()

class SQLServerConnector(DataProviderInterface):
    def get_cool_data(self):
        return "cool data"

class SQLServerDataProvider(DataProvider, SQLServerConnector):
    pass

db = SQLServerDataProvider()
print(db.get_cool_data())

So I implemented this and I was like, yay, it’s working.

But looking at it, it seems like a lot of code.

As python has ducktyping, another and shorter way would be to just do this:

class SQLServerDataProvider():
    def get_cool_data(self):
        return "cool sqlserver data"

class MongoDataProvider():
    def get_cool_data(self):
        return "cool mongodb data"

db = SQLServerDataProvider()
print(db.get_cool_data())

Do you see any advantage in using the super() implementation?
How would you engage such a problem?

I’m really glad for any input as I can’t seem to wrap my head around the problem

By HKR via Discussions on Python.org at 17Jun2022 14:06:

So I thought, hey, I’d use it in one of my projects, where I have a
data analysis script which gets data from different data sources. In C#
I would implement an interface for the datasource provider so that it
is exchangable in the data analyser script.

class DataProviderInterface():
   def get_cool_data(self):
       raise NotImplementedError()

Modern Python has formar support for abstract classes like this above:

from abc import ABC, abstractmethod
class DataProviderInterface(ABC):
    @abstractmethod
    def get_cool_data(self):
        raise NotImplementedError

This looks like more boilerplate, but it has the advantage that if you
derive a subclass from it, you can’t actually instantiate a new instance
unless you have provided nonabstract (“concrete”) implementations for
all the methods in the interface.

Side note: you can just raise an exception type if you’re not providing
any parameters, and Python will make the exception instance for you.
Saves some brackets in this situation.

Then:

class DataProvider(DataProviderInterface):
    def get_cool_data(self):
        return super().get_cool_data()

I wouldn’t bother with this class at all. Just use
DataProviderInterface below. This is because the class doesn’t add
anything to DataProviderInterface.

Also, any method like get_cool_data which just calls the superclass
method can also be omitted - subclasses will get it from the superclass
anyway.

Then:

class SQLServerConnector(DataProviderInterface):
    def get_cool_data(self):
        return "cool data"

This is a bit odd. I’d expect a SQLServerConnector to provide some
mechanism to “connect”, based on the name.

class SQLServerDataProvider(DataProvider, SQLServerConnector):
    pass

You could just inherit from SQLServerConnector here. Again, because
DataProvider does not provide anything useful.

db = SQLServerDataProvider()
print(db.get_cool_data())


So I implemented this and I was like, yay, it's working.

But looking at it, it seems like a lot of code.

Yes. For me I’d define: DataProviderInterface and on the face of it,
just SQLServerDataProvider with the method from SQLServerConnector.
Though I might misunderstand your intent here.

So: I’d want the interface abstract class. I’d want an implementation.
2 classes. There might be other classes if your needs become more
complex.

As python has ducktyping, another and shorter way would be to just do this:

class SQLServerDataProvider():
   def get_cool_data(self):
       return "cool sqlserver data"

class MongoDataProvider():
   def get_cool_data(self):
       return "cool mongodb data"

db = SQLServerDataProvider()
print(db.get_cool_data())

Do you see any advantage in using the super() implementation?

Well, your super() call was redundant because nothing new was added.

The benefit of the interface class is that you can define what
behaviours/methods all your implementations must provide. And using
ABC will cause Python to enforce providing the necessary things. It
helps you clarify what you need all your concrete classes to do.

Also, a superclass lets you use type annotations later to specify that
something works with a DataProviderInterface.

Side note: one informal way to define the interface class is to start
with the concrete class first, make it work. Then when you want
another, similar, concrete class (eg for a different database type)
you can look at your first implementation, decide what is common, pull
that into the base class, then make both concrete classes inherit from
the base class at that point.

The time you want super() is when you’re adding functionality on top
of what the superclass does. For example, made up:

class MongoDataProvider(DataProviderInterface):
    def __init__(self, mongo_db_param):
        super().__init()
        self.mongo_info = mongo_db_param

Here you’re calling the superclass’ __init__ method to do “whatever it
does”, and then saving some additional information pertinent to MongoDB
stuff. Notice that you’re not passing the mongo_db_param to the
superclass method - it is generic.

How would you engage such a problem?
I’m really glad for any input as I can’t seem to wrap my head around
the problem

[ Rummages around for something useful which isn’t very complex… ]

Ok, I’ve got a little Tk application, and it has a little GUI module
with widgets which subclass the basic Tk widgets. Here’s what it uses
for a Button. This is, again, overly complex in some ways:

# local shims for the tk and ttk widgets
BaseButton = tk.Button
is_tk_button = True
if is_darwin:
  try:
    from tkmacosx import Button as BaseButton
    is_tk_button = False
  except ImportError as e:
    warning(
        "import tkmacosx: %s; buttons will look better with tkmacos on Darwin",
        e
    )

# pylint: disable=too-many-ancestors
class Button(_Widget, BaseButton):
  ''' Button `_Widget` subclass.
  '''

  def __init__(self, *a, background=None, highlightbackground=None, **kw):
    if background is None:
      background = self.WIDGET_BACKGROUND
      if not is_tk_button:
        kw.update(background=background)
    if highlightbackground is None:
      highlightbackground = background
    kw.update(highlightbackground=highlightbackground)
    super().__init__(*a, **kw)

As I said, overly complex. This has 2 parts:

  • an intro where I look for my preferred button base class. We start
    with tk.Button, but use tkmacosx.Button if it is available.
  • my personal Button class which subclasses the base button class I
    chose (ignore _Widget, I can get into that later if you care).

So the __init__ method in my class does a little extra work then calls
the __init__ of the superclass via super(). In particular, because
I’m having trouble getting the background colours behaving as I want, my
Button accepts 2 special keyword parameters: background and
highlightbackground, intended for the button background and the
background when the mouse is over the button.

If background is unspecified (it defaults to None) I use the
self.WIDGET_BACKGROUND background. This comes from my _Widget class
and is perhaps a poor excuse for the “proper” way to manage decor.

Then if highlightbackground is unspecified, it gets its value from
background.

Then it stuffs highlightbackground back into the remaining keyword
parameters. Then we call super().__init__(*a,**kw) to run the
superclass __init__ now that we’ve done our special setup.

Now, __init__ is just a normal method, like any other! By the time it
is called your object already exists - its sole purpose is to fill out
attributes, open databases, whatever additional work is needed before
you call any other methods.

The key thing here is that this method does additional stuff, but
still calls the original method via super() to ensure the “normal”
base class stuff also gets done. It is the same with any other
instance method. If you’d like a non-__init__ example, here’s another
widget from the same module:

class ThumbNailScrubber(Frame, _FSPathsMixin):
  ''' A row of thumbnails for a list of filesystem paths.
  '''
  [...]
  def set_fspaths(self, new_fspaths):
    ''' Update the list of filesystem paths.
    '''
    display_paths = super().set_fspaths(new_fspaths)
    for child in list(self.grid_slaves()):
      child.grid_remove()
    for i, dpath in enumerate(display_paths):
      thumbnail = self.make_subwidget(i, dpath)
      thumbnail.grid(column=i, row=0)

This displays a row of thumbnails. It inherits from Frame (the Tk
widget which will hold the thumbnails) and _FSPathsMixin which is a
class in the same module for holding a list of filesystem paths.

All _FSPathsMixin subclasses have a .set_fspaths() method to update
the list of paths to manage. In _FSPathsMixin itself this method
basicly just saves the new list of paths. In ThumbNailScrubber we
first call super().set_fspaths(new_fspaths) to save the new paths,
then we also update the thumbnails displayed by removing all the old
thumbnails and making new ones.

Again, this is a method where we do additional work. We use super()
to call the original method to do the core work common to every class,
and then do our subclass-specific work after that.

Cheers,
Cameron Simpson cs@cskk.id.au

1 Like

Dependency injection has nothing to do with super or inheritance. Dependency injection is a 10 cent concept dressed up in complicated language to make it look like a $1000 concept.

This is a class without dependency injection:

class Car:
    def __init__(self):
        self.engine = Engine()

car = Car()

The class automatically chooses what sort of engine the car gets, and the caller has no choice but to accept it.

This is a class with dependency injection:

class Car:
    def __init__(self, engine):
        self.engine = engine

car = Car(Engine())

Now the caller can choose what sort of engine to fit to the car. So long as it has the same interface as the standard Engine, you can use any engine-like class you want:

car = Car(Souped_Up_Nitro_Burning_MegaEngine(power='AWESOME!'))

That is all. There is nothing whatsoever to do with super in dependency injection.

1 Like
class DataProviderInterface():
    def get_cool_data(self):
        raise NotImplementedError()

class DataProvider(DataProviderInterface):
    def get_cool_data(self):
        return super().get_cool_data()

That’s not going to work. If you call DataProvider().get_cool_data() it will use the method defined in DataProviderInterface, which will immediately raise.

(Personally, I think that abstract classes like DataProviderInterface are major overkill for most applications. Unless you have, oh, at least half a dozen subclasses, or a strong need to use isinstance(), it is easier to just write the classes independently. But I digress.)

DataProvider doesn’t appear to do anything useful. So dump it! Every class needs to pull its weight, Python is not like Java which insists on creating enormous class hierarchies for the tiniest thing.

https://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html

In Python, every additional class in the hierarchy has a real performance cost. Keep your class hierarchies shallow and lean.

Another issue: there is never any need to write a method which does
nothing but call super(). Your DataProvider class could be
simplified to:

class DataProvider(DataProviderInterface):
    pass

“So I implemented this and I was like, yay, it’s working. But looking at it, it seems like a lot of code.”

That’s because it is a lot of code, for no good reason. Most of the classes seem like they are unneeded.

Your duck-typing solution seems perfectly adequate to me. The only reason why I wouldn’t use it is if

  1. You have an arbitrarily large number of possible classes, not just 2 or 3, and need to be able to test for them with isinstance();
  2. And they share significant amounts of code.

Even then, I would try to keep the inheritence hierarchies shallow.

1 Like