1

Below is a simplified version of the code I have:

class Base:
    def __new__(klass, *args):
        N = len(args)
        try:
            return [Sub0, Sub1, Sub2][N](*args)
        except IndexError:
            raise RuntimeError("Wrong number of arguments.") from None

class Sub0(Base): pass
class Sub1(Base): pass
class Sub2(Base): pass

This code does not work. My understanding for the reason it doesn't work is that my base class definition is dependent upon definition of the subclasses, which are in turn dependent on the base class.

What I am trying to accomplish is to create an API (for myself) in which I can do things like the following:

obj = Base(1,2)
assert type(obj) == Sub2
obj = Base()
assert type(obj) == Sub0
assert isinstance(obj, Base)

I might be asked why, exactly, I want to be able to write code like this. The answer is it seems like it will be useful for a project I'm working on. However, I am considering abandoning the subclasses and doing it this way instead:

obj = Base(1,2)
assert obj.type == "Sub2"
obj = Base()
assert obj.type == "Sub0"
assert isinstance(obj, Base)

As a relatively inexperienced programmer, I'm still trying to figure out what I should be doing for my particular problem.

However, the focus of this question is: if there are situations where using the base class and subclasses in this way makes sense, how can I make this work the way I have described? On the other hand, if it definitely does not ever - or at least, very rarely - make sense to try and do this (in Python, I mean - Python is different from other languages), why is this true?

Rick supports Monica
  • 33,838
  • 9
  • 54
  • 100

4 Answers4

2

Your design doesn't particularly make sense ... Generally, it's a bit strange for a base class to know about it's ancestors. The information flow should go the other way around. The particular problem with your code is that you call:

Base(1, 2)

which calls Base.__new__. It picks out one of the subclasses (Sub2 in this example) and then (essentially) does:

return Sub2(1, 2)

However, this calls Sub2.__new__ which just so happens to be Base.__new__ and so you have recursion problems.

A much better design is to forget about having Base pick out the subclass... Make your classes as you normally would:

class Sub0:
    ...

class Sub1:
    ...

class Sub2:
    ...

And then have a factory function do what Base is doing now.

# This might be a bad name :-)
_NARG_CLS_MAP = (Sub0, Sub1, Sub2) 

def factory(*args):
    n = len(args)
    try:
        return _NARG_CLS_MAP[n](*args)
    except IndexError as err:
        raise RuntimeError('...') from err

This avoids any sort of weird recursion and strange inheritance tree problems.

mgilson
  • 264,617
  • 51
  • 541
  • 636
1

This is gonna be a short answer as i am on my phone. First, check out this.

Now, although you might find a way of doing it (there is a way in c++ not sure about python) it is a bad design. You shouldn't have an issue like this. A parent cannot be dependent of its child. You missed the point of inheritance. It is like saying a parent class car depends on its children: Bentley or Mazeratti. Right now, I can think of 2 scenarios that you might have:

  1. If two classes really depend on each other (one object is LIKE the other), then there can be another base for both, which will contain the sharing part of both.

  2. If there cannot be a class base for both (one object is PART of another), you do not need inheritance at all.

In short, it really depends on your problem, try to fix the cause of such problem. You should change the design of your objects.

Community
  • 1
  • 1
khajvah
  • 4,049
  • 8
  • 34
  • 56
1

Taking a guess about your requirements, it's almost as if you want to have multiple constructors for a class, each with a different number of arguments, i.e. multiple dispatch on arguments. You can use the multipledispatch module to overload __init__, for example:

from multipledispatch import dispatch

class Thing(object):

    @dispatch(object)
    def __init__(self, arg1):
        """Thing(object): create a Thing with single argument."""
        print "Thing.__init__(): arg1 %r" % (arg1)

    @dispatch(object, object)
    def __init__(self, arg1, arg2):
        """Thing(object): create a Thing with two arguments."""
        print "Thing.__init__(): arg1 %r, arg2 %r" % (arg1, arg2)

    @dispatch(object, object, object)
    def __init__(self, arg1, arg2, arg3):
        """Thing(object): create a Thing with three arguments."""
        print "Thing.__init__(): arg1 %r, arg2 %r, arg3 %r" % (arg1, arg2, arg3)

    def normal_method(self, arg):
        print "Thing.normal_method: arg %r" % arg

Thing.__init__(): arg1 1
>>> thing = Thing('a', 2)
Thing.__init__(): arg1 'a', arg2 2
>>> thing = Thing(1, 2, 'three')
Thing.__init__(): arg1 1, arg2 2, arg3 'three'
>>> thing = Thing()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 235, in __call__
    func = self.resolve(types)
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 184, in resolve
    (self.name, str_signature(types)))
NotImplementedError: Could not find signature for __init__: <>
>>> thing = Thing(1, 2, 3, 4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 235, in __call__
    func = self.resolve(types)
  File "/home/mhawke/virtualenvs/urllib3/lib/python2.7/site-packages/multipledispatch/dispatcher.py", line 184, in resolve
    (self.name, str_signature(types)))
NotImplementedError: Could not find signature for __init__: <int, int, int, int>

So this might be a solution for you if you don't actually require separate sub classes, just different ways of constructing them.

mhawke
  • 75,264
  • 8
  • 92
  • 125
  • I did not know about `multipledispatch` - I'll have to look into it some more. – Rick supports Monica Mar 13 '15 at 13:32
  • OK. `multipledispatch.dispatch` rules. I've used `overload.overload` a bit, but this is a lot better. – Rick supports Monica Mar 13 '15 at 17:45
  • Do you happen to know if `multipledispatch.dispatch` has a way to add a method at the end that catches any and all arguments? I'm looking through the documentation and don't see a way to do that. What I am trying to do is add a fourth version of `__init__` with the signature of `(self, *args, **kwargs)` so that I can have custom behavior for all other argument combinations not accounted for in previous `@dispatch` decorated versions. – Rick supports Monica Mar 14 '15 at 02:42
  • Unfortunately varargs is not supported. You can get keyword args to work by declaring the function like `def __init__(self, arg1, **kwargs):` and it will still dispatch to this version of the function having 1 positional argument. But, `*args` doesn't work. There is no standard way to mark a function as a "catchall", although a hack is possible that can kind of make it work. Given this limitation you might want to consider [mgilson's answer](http://stackoverflow.com/a/29025568/21945) for it's simplicity and flexibility. – mhawke Mar 15 '15 at 10:13
1

In reading the other answers and thinking about this a bit more, I have come up with a way to accomplish what I set out to do as described in the question. However, I do not necessarily think it is a good design; I'll leave that to people with more experience than me.

This approach uses functionality found in the abstract base classes or abc module to override isinstance so it behaves the way we want.

from abc import ABCMeta

class RealBase: 
    '''This is the real base class; it could be an abc as well if desired.'''
    def __init__(*args):
        pass

class Sub0(RealBase): pass
class Sub1(RealBase): pass
class Sub2(RealBase): pass

class Base(metaclass = ABCMeta):
    '''Base is both an abstract base class AND a factory.'''
    def __new__(klass, *args):
        N = len(args)
        try:
            return [Sub0, Sub1, Sub2][N](*args)
        except IndexError:
            raise RuntimeError("Wrong number of arguments.") from None

#Now register all the subclasses of RealBase with the Base abc
for klass in RealBase.__subclasses__():
    Base.register(klass)

if __name__ == "__main__":
    obj = Base(1,2)
    assert type(obj) == Sub2
    obj = Base()
    assert type(obj) == Sub0
    assert isinstance(obj, Base)

Although I'm not certain, I believe the design approach I have used above is a sound one, as this kind of thing is what the abc module seems to have been created for in the first place (providing a way to overload the isinstance() and issublcass() functions).

Community
  • 1
  • 1
Rick supports Monica
  • 33,838
  • 9
  • 54
  • 100