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