Search code examples
pythonsubclassing

factory class, wrong number of arguments being passed to subclass constructor


I was looking at Python: Exception in the separated module works wrong which uses a multi-purpose GnuLibError class to 'stand in' for a variety of different errors. Each sub-error has its own ID number and error format string.

I figured it would be better written as a hierarchy of Exception classes, and set out to do so:

class GNULibError(Exception):
    sub_exceptions = 0  # patched with dict of subclasses once subclasses are created
    err_num = 0
    err_format = None

    def __new__(cls, *args):
        print("new {}".format(cls)) # DEBUG
        if len(args) and args[0] in GNULibError.sub_exceptions:
            print("  factory -> {} {}".format(GNULibError.sub_exceptions[args[0]], args[1:])) # DEBUG
            return super(GNULibError, cls).__new__(GNULibError.sub_exceptions[args[0]], *(args[1:]))
        else:
            print("  plain {} {}".format(cls, args)) # DEBUG
            return super(GNULibError, cls).__new__(cls, *args)

    def __init__(self, *args):
        cls = type(self)
        print("init {} {}".format(cls, args)) # DEBUG
        self.args = args
        if cls.err_format is None:
            self.message = str(args)
        else:
            self.message = "[GNU Error {}] ".format(cls.err_num) + cls.err_format.format(*args)

    def __str__(self):
        return self.message

    def __repr__(self):
        return '{}{}'.format(type(self).__name__, self.args)

class GNULibError_Directory(GNULibError):
    err_num = 1
    err_format = "destination directory does not exist: {}"

class GNULibError_Config(GNULibError):
    err_num = 2
    err_format = "configure file does not exist: {}"

class GNULibError_Module(GNULibError):
    err_num = 3
    err_format = "selected module does not exist: {}"

class GNULibError_Cache(GNULibError):
    err_num = 4
    err_format = "{} is expected to contain gl_M4_BASE({})"

class GNULibError_Sourcebase(GNULibError):
    err_num = 5
    err_format = "missing sourcebase argument: {}"

class GNULibError_Docbase(GNULibError):
    err_num = 6
    err_format = "missing docbase argument: {}"

class GNULibError_Testbase(GNULibError):
    err_num = 7
    err_format = "missing testsbase argument: {}"

class GNULibError_Libname(GNULibError):
    err_num = 8
    err_format = "missing libname argument: {}"

# patch master class with subclass reference
# (TO DO: auto-detect all available subclasses instead of hardcoding them)
GNULibError.sub_exceptions = {
    1: GNULibError_Directory,
    2: GNULibError_Config,
    3: GNULibError_Module,
    4: GNULibError_Cache,
    5: GNULibError_Sourcebase,
    6: GNULibError_Docbase,
    7: GNULibError_Testbase,
    8: GNULibError_Libname
}

This starts out with GNULibError as a factory class - if you call it with an error number belonging to a recognized subclass, it returns an object belonging to that subclass, otherwise it returns itself as a default error type.

Based on this code, the following should be exactly equivalent (but aren't):

e = GNULibError(3, 'missing.lib')
f = GNULibError_Module('missing.lib')

print e  # -> '[GNU Error 3] selected module does not exist: 3'
print f  # -> '[GNU Error 3] selected module does not exist: missing.lib'

I added some strategic print statements, and the error seems to be in GNULibError.__new__:

>>> e = GNULibError(3, 'missing.lib')

new <class '__main__.GNULibError'>
  factory -> <class '__main__.GNULibError_Module'> ('missing.lib',)  # good...
init <class '__main__.GNULibError_Module'> (3, 'missing.lib')        # NO!
                                            ^
                                           why?

I call the subclass constructor as subclass.__new__(*args[1:]) - this should drop the 3, the subclass type ID - and yet its __init__ is still getting the 3 anyway! How can I trim the argument list that gets passed to subclass.__init__?


Solution

  • You cannot affect what is passed to __init__, as long as you're doing it with a "factory class" like you have now that is returning subclasses of itself. The reason the "3" argument is still passed is because you are still returning an instance of GNULibError from __new__. By the time __new__ is called, it's too late to decide what will be passed to __init__. As stated in the documentation (emphasis added):

    If __new__() returns an instance of cls, then the new instance’s __init__() method will be invoked like __init__(self[, ...]), where self is the new instance and the remaining arguments are the same as were passed to __new__().

    In other words, when you call GNULibError(3, 'missing.lib'), it's too late --- by calling the class with those arguments you have ensured that those are the arguments that will be passed to __init__. __new__ can return a different instance than you might otherwise get, but it can't stop the normal initialization from happening.

    As suggested by @Ned Batchelder, you are better off using a factory function instead of a "factory class", because a function doesn't have this __new__/__init__ machinery and you can just return an instance of the class you want.