Friday Finking: Abstract Base Classes - love or hate

dn PythonList at DancesWithMice.info
Thu Jan 14 19:43:03 EST 2021


Do you make frequent use of Abstract Base Classes (ABCs), prefer to use
an ordinary super-class for the same purpose, or steer-clear? Are they
more-usually employed when the project includes an extensive design
stage, and the meta-class integral to some hierarchy of entities?


Previous Friday Finkings have arisen from in-house discussions within a
Python dev.team which deserved a wider airing or debate. This contains
(with due apologies) excessive personal reflection and comments
abstracted from posts to this Discussion List.


Many of us have used Python for years but never touched ABCs. Apparently
then, we don't need to be aware of them! Colleagues 'here' have said
"never used it" and/or "never seen the need", and similar.

Often pythonista learn how to define class(es), and then to inherit or
compose further classes. Everything works swimmingly. If it ain't broke,
don't fix it! So, we have no reason to advance our programming
capabilities/knowledge. Indeed an earlier post voiced the suggestion
that we could get on quite happily without knowing about/using ABCs.


Aside: Certainly the earliest iteration of Python meta-classes/ABCs
(related, but confusingly different - am not going there!) 'arrived' at
about the same time as did I - back at Python 1.n - but I've only
ever/started using ABCs, since circa 3.5. Thus, from personal
learning-experience: one can only understand and use abstract-classes
after gaining a thorough understanding and facility with using
(ordinary) class-es. (perhaps?)


Conducting (what some people call) extensive research*, we will be told
that an ABC is: "a way to overload isinstance() and issubclass()". When
would we use those two functions which introspect a class-hierarchy?

isinstance(object, classinfo)
= is the object argument an instance of the classinfo argument

issubclass(class, classinfo)
= is the class argument a subclass of the classinfo argument
(etc)

Luv-a-duck! How does this fit with duck-typing and EAFP? (sorry,
terrible attempt at a joke) Aren't we supposed to *presume* that we our
sub-class/instance is just that, and we should just presume to use its
data and methods (wrapped in a try...except in case 'forgiveness' might
be necessary)?


Perhaps with the increasing number of DataScience folk using Python (?),
we do seem to be finding a reasonable proportion of Python
improvements/updates seemingly heading in the LBYL direction... OK,
let's go-with-the-flow.

There are two 'problems' with the isinstance()/issubclass() approach:
- it's not very duck-y
- when it 'happens'.

So, let's say we want to *execute* a particular method against the
current data-structure. We can only do this if the class (or its
hierarchy) includes said method. Thus:

    data.method( arguments... )

However, we are concerned that the instance may not be part of a
hierarchy offering this method. What can we do?

- EAFP: wrap in try...except
- LBYL: check for method by assuring the hierarchy, eg isinstance()

Each incurring the consequential-problem: but what then?


The problem is not 'discovered' until execution-time - worst-case
scenario: it is discovered by some user (who is no-longer our friend).

If we are really going LBYL, surely we want to discover the problem as
early as possible, ie preferably whilst coding+testing?

Using an ABC we can do just that, because the presence/absence of
methods is checked when the sub-class is defined. If it does not
implement all of the required methods, the sub-class will fail. Yes, we
could do that by coding a super-class with methods that contain only:

    raise NotImplementedError()

but when would a sub-class's error in not implementing the method then
be 'discovered'?


Personal aside/illustration/confession: I implemented an application
which used "plug-ins" - effectively filters/actions. The first couple of
use-cases were easy - they were integral to the application-design. As
is the way of these things, few months later a request arrived for
another plug-in. Of course, I was very happy with the plug-in which I
coded oh-so-quickly. Trouble is, I *forgot* to implement one of the
required methods, and mis-spelled another. Yes, if I had a scrap of
humility, I should have re-read my original code. Sigh!

Forget the apocryphal "will your future self remember in six months'
time", as I have aged (and most of us do!), I've moved my philosophy to
one of trying to avoid having to remember - firstly in six *weeks'*
time, and more recently: six *days'*!

All was greatly improved by implementing an ABC as a guide, or a
check-list, even, a template - as proven by later requests for yet more
'plug-ins'...

Please remember that 'umble scribe is not an "OOP-native", starting this
career way-back when mainframes were powered by dinosaurs in
tread-mills. I am told this is what other languages refer to as an
"interface". In some ways I visualise the ABC as a "mixin" - perhaps
better-understood when the ABC does include an inheritable method.
(feel free to add/discuss/correct...)


Back to the "Finking":

When faced with a 'green-field' project, and a dev.team sits down to
create a solid overview design, I've noticed architects and experienced
designers 'see' where meta-classes should be employed and specify (and
are probably the ones to build), accordingly.

My experience with smaller projects is that TDD directs us to develop
one functional-step at a time. Once successfully tested, we re-factor
and consider qualitative factors. Either the former or latter step may
involve a decision to sub-class an existing structure, or similar. It is
possible that such may include opportunity (even, highlight that it
would be good sense) to employ an ABC and derive from there - because at
this time we may better-appreciate opportunities for re-use, plus having
the tests in-place reduces fear of breakage!

However, as described, my usage seems to be less about data-structures
and more like a check-list of functionality - which I might otherwise
forget to implement in a sub-class.


So, does that imply that when there is a wide and detailed design stage
meta-classes may be employed 'from the word go'; whereas if we employ a
more bottom-up approach, they only make an appearance as part of a 'make
it better' process?


Web.Refs:
(warning - much of the stuff 'out there' on the web is dated and
possibly even misleading to users of 'today's Python'!)
https://www.python.org/dev/peps/pep-3119/
https://docs.python.org/3/library/abc.html
https://docs.python.org/3/library/functions.html#isinstance
https://docs.python.org/3/library/functions.html#issubclass
https://idioms.thefreedictionary.com/Lord+love+a+duck!
https://www.tutorialspoint.com/abstract-base-classes-in-python-abc
https://devtut.github.io/python/abstract-base-classes-abc.html
https://everyday.codes/python/abstract-classes-and-meta-classes-in-python/

* ie the top two 'hits' returned by DuckDuckGo
- mere "research" involves only taking the first/"I feel lucky"


Sample code:

from abc import ABC, abstractmethod


class Image( ABC ):
    def __init__( self, name )->None:
        self.name = name

    @abstractmethod
    def load( self, filename:str ):
        """Load image from file into working-memory."""

    @abstractmethod
    def save( self, filename:str ):
        """Save edited image to file-system."""


try:
    i = Image()
except TypeError:
    print( "Sorry, can't instantiate i/Image directly" )


class Non_Image( Image ):

    def load_file( self, filename:str ):
        """This looks reasonable."""


try:
    n = Non_Image( "Under Exposed" )
except TypeError:
    print( "Sorry, n/Non_Image does not look good" )


class PNG_Image( Image ):

    def load( self, filename:str ):
        """Load PNG file."""

    def save( self, filename:str ):
        """Save to PNG file."""

p = PNG_Image( "Picture of the Artist as a Young Man" )
print( "p has instantiated successfully" )
-- 
Regards,
=dn


More information about the Python-list mailing list