1

Suppose I have the following base and child

class Base:

    def __new__(cls, *args):
        if cls is Base:
            if len(args) < 2:
                return Child1.__new__(Child1, *args)

            return Child2.__new__(Child2, *args)

        return super().__new__(cls)

    def __init__(self, arg):
        self.common_arg = arg


class Child1(Base):
    def __init__(self, arg0=None):
        super().__init__(arg0)



class Child2(Base):
    def __init__(self, arg0, arg1, *args):
        super().__init__(arg0 + arg1)

        self.args = list(args).copy()

There is clearly a circular dependency in between the classes, but, as long as all the classes are defined in the same module this does not cause any problems.

Now, how should I split them into three modules (in the same package)?

I did the split in three files:

package/
    __init__.py
    base.py
    ch1.py
    ch2.py

with the following contents:

# base.py ############################################################

from . import ch1, ch2

class Base:

    def __new__(cls, *args):
        if cls is Base:
            if len(args) < 2:
                return ch1.Child1.__new__(ch1.Child1, *args)

            return ch2.Child2.__new__(ch2.Child2, *args)

        return super().__new__(cls)

    def __init__(self, arg):
        self.common_arg = arg


# ch1.py ############################################################
from . import base

class Child1(base.Base):
    def __init__(self, arg0=None):
        super().__init__(arg0)

# ch2.py ############################################################
from . import base


class Child2(base.Base):
    def __init__(self, arg0, arg1, *args):
        super().__init__(arg0 + arg1)
        self.args = list(args).copy()   

as suggested here but it doesn't work.

import package.ch1

raises

AttributeError: module 'package.base' has no attribute 'Base'
martineau
  • 119,623
  • 25
  • 170
  • 301
MikeL
  • 2,369
  • 2
  • 24
  • 38
  • Not sure that's exactly the same problem, but I had a dependency problem once, and I found the classical hack to put the import in a function without parameters, containing only the import statement. – Guilhem L. Feb 11 '20 at 14:00
  • 2
    You can't import a module that uses classes requiring other classes that haven't been defined. You can almost certainly design this more effectively. Take advantage of the fact that methods in base classes are by default overridden by inheriting classes and you don't need this confusing design. – h0r53 Feb 11 '20 at 14:03
  • 1
    Normally it's an anti-pattern to use architecture where parent class is obliged to know something about his children and their implementation. What exact task you're trying to solve with this trick? – Hyyudu Feb 11 '20 at 14:11
  • 1
    Whatever hack you could use, it won't solve the root issue, which is that your design is just wrong (wrt: both basic OO design - base classes should not know about their children - and dependencies handling - even if the language support it, you _still_ shouldn't have any circular dependencies. Obvious XY problem (you're asking how to make the wrong solution work instead of explaining the problem _behind_ this solution and how to solve it), so please edit your question and explain the real use case so someone might suggest a better design. – bruno desthuilliers Feb 11 '20 at 14:14
  • I agree with all of you that this design pattern is complicated, though this is something that has been used, for example in `scipy.signal.lti` (https://docs.scipy.org/doc/scipy/reference/generated/scipy.signal.lti.html). The reason I am using this pattern is the following: `Child1` and `Child2` are different representations of the `Base`. They will have many common methods but the implementation of methods are different. The client simply instantiates `Base` and let the code figure out which representation is more suitable `Child1` or `Child2` for that problem. – MikeL Feb 11 '20 at 14:19
  • How will your users instantiate one of these classes? Is it `c = Child1(args)` or `c = Base(args)`? Perhaps you need a factory function instead. – quamrana Feb 11 '20 at 14:25
  • You're returning instance from another class from within the __new___, this may break `isinstance` I guess. – geckos Feb 11 '20 at 14:27
  • I agree with @bruno that the base class knowing about its derived classes is a poor design. There are better ways to implement such things, such as by having a dynamic "registry" of subclasses that allows deferring creation to the appropriate one at run time. As of version Py 3.6, there's also [`__init_subclass__`](https://docs.python.org/3/reference/datamodel.html#object.__init_subclass__) which makes implementing something like that fairly easy. – martineau Feb 11 '20 at 14:27
  • @quamrana The user instantiates the class as `c = Base(args)`. I agree with you that I should change my design pattern, make `Base` an abstract base class, and use a factory function to instantiate the classes when the user does not want to explicitly mention `Child1` or `Child2` – MikeL Feb 11 '20 at 15:01

1 Answers1

0

Make your users call a Factory Function:

def make_base(*args):
    if len(args) < 2:
        return Child1(*args)

    return Child2(*args)


class Base:
    def __init__(self, arg):
        self.common_arg = arg


class Child1(Base):
    pass        # Child1 automatically inherits Base.__init__()


class Child2(Base):
    def __init__(self, arg0, arg1, *args):
        super().__init__(arg0 + arg1)

        self.args = list(args).copy()

Now each part of the above code can be split into its own file.

quamrana
  • 37,849
  • 12
  • 53
  • 71
  • Thank you. For clarification, I must add that the factory function `make_base` and the definition of the base class `Base` cannot be in the same file if we want to split the code into separate files. Two separate files for `Base` and the factory function are necessary. – MikeL Feb 11 '20 at 15:37
  • Yes, I did mean that. There is one function and three classes. They can be split into four files. – quamrana Feb 11 '20 at 15:40