python: abstract base class' __init__(): initializion or validation? [closed]
Asked Answered
H

3

17

class ABC is an "abstract base class". class X is its subclass.

There's some work that needs to be done in any subclass of ABC, which is easy to forget or do incorrectly. I'd like ABC.__init__() to help catch such mistakes by either:

(1) starting that work, or (2) validating it

This impacts whether super().__init__() is called at the start or at the end of X.__init__().

Here's a simplified example for illustration purposes:

Suppose every subclass of ABC must have an attribute registry, and it must be a list. ABC.__init__() can either (1) initialize registry or (2) check that it was properly created. Following is the sample code for each approach.

Approach 1: initialize in ABC

class ABC:
    def __init__(self):
        self.registry = []

class X:
    def __init__(self):
        super().__init__()
        # populate self.registry here
        ...

Approach 2: validate in ABC

class ABC:
    class InitializationFailure(Exception):
        pass
    def __init__(self):
        try:
            if not isinstance(self.registry, list):
                raise ABC.InitializationError()
        except AttributeError:
            raise ABC.InitializationError()

class X:
    def __init__(self):
        self.registry = []
        # populate self.registry here
        ...
        super().__init__()

Which is a better design?

Hemihydrate answered 27/2, 2011 at 12:43 Comment(0)
A
16

Certainly, one prefers approach 1 to approach 2 (as approach 2 relegates the base to a tag interface rather than fulfilling abstract functionality). But, approach 1 doesn't, by itself, meet your goal of preventing the subtype developer from forgetting to implement the super() call properly, ensuring initialization.

you may want to look into the "Factory" pattern to alleviate the possibility of subtype implementers forgetting initialization. Consider:

class AbstractClass(object):
    '''Abstract base class template, implementing factory pattern through 
       use of the __new__() initializer. Factory method supports trivial, 
       argumented, & keyword argument constructors of arbitrary length.'''

   __slots__ = ["baseProperty"]
   '''Slots define [template] abstract class attributes. No instance
       __dict__ will be present unless subclasses create it through 
       implicit attribute definition in __init__() '''

   def __new__(cls, *args, **kwargs):
       '''Factory method for base/subtype creation. Simply creates an
       (new-style class) object instance and sets a base property. '''
       instance = object.__new__(cls)

       instance.baseProperty = "Thingee"
       return instance

This base class can be extended more trivially than in approach 1, using only three (3) lines of code san-commment, as follows:

class Sub(AbstractClass):
   '''Subtype template implements AbstractClass base type and adds
      its own 'foo' attribute. Note (though poor style, that __slots__
      and __dict__ style attributes may be mixed.'''

   def __init__(self):
       '''Subtype initializer. Sets 'foo' attribute. '''
       self.foo = "bar"

Note that though we didn't call the super-class' constructor, the baseProperty will be initialized:

Python 2.6.1 (r261:67515, Jun 24 2010, 21:47:49) 
[GCC 4.2.1 (Apple Inc. build 5646)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from TestFactory import *
>>> s = Sub()
>>> s.foo
'bar'
>>> s.baseProperty
'Thingee'
>>> 

As its comment indicates, the base class AbstractClass need not use slots, it could just as easily 'implicitly' define attributes by setting them in its new() initializer. For instance:

instance.otherBaseProperty = "Thingee2"

would work fine. Also note that the base class' initializer supports trivial (no-arg) initializers in its subtypes, as well as variable-length arugmented and keyword argument initializers. I recommend always using this form as it doesn't impose syntax in the simplest (trivial constructor) case but allows for the more complex functionality without imposing maintenance.

Apul answered 27/2, 2011 at 14:9 Comment(0)
C
2

In the example you have provided, I would do it as in your approach 1. I would see class ABC mainly as an implementation helper for X and other classes that implement a certain interface, however. Said interface consists of the attribute 'registry'.

You should, logically at least, discern between the interface shared by X and other classes, and the baseclass which helps you implement it. That is, define separately that there is an interface (for example "ABC"), which exposes a list "registry". Then, you may decide to factor out the implementation of the interface as a common baseclass (conceptually a mix-in) to implementors of interface ABC, since it makes it very easy to introduce new implementing classes (in addition to X).

Edit: With regard to guarding against mistakes in implementing classes, I'd target this through unit tests. I think this is more comprehensive than trying to account for everything in your implementation :)

Colton answered 27/2, 2011 at 13:49 Comment(0)
C
1

The first is the better design, because the subclasses don't need to know you have implemented the registry with a list. For instance, you could offer an _is_in_registry function which takes one argument, and returns whether the element is in the registry. You could then later change the superclass and replace the list by a set, because elements canonly appear once in the registry, and you wouldn't need to change the subclasses.

Also, it is less code: Imagine you have 100 fields like this in ABC and ABC has 100 subclasses like X...

Cork answered 27/2, 2011 at 12:47 Comment(3)
Sounds reasonable. So I should try not to rely on registry being a list, when I populate it in the subclass, correct? That is, rather than use registry.append(new_item) and registry[-1] = new_item freely, I should try and use self.add_to_registry(new_item), which will be defined in ABC to .append(), but could be changed to .add() later if needed?Hemihydrate
@Hemihydrate That's how I would do it, yes. I'm not sure if it's the 'pythonic' way, I'm just starting out with python, I'm more from a static languages background. But if you're not adding those functions, why build an abstract superclass at all?Cork
Well, I might use ABC just to validate stuff.. But I agree, it seems it's more useful to actually do some work.Hemihydrate

© 2022 - 2024 — McMap. All rights reserved.