0

I'm coding in Python. I have a Base class which contains several methods. There are some child classes; they may have their own methods. I want the Base class constructor to create the object not of the class itself but the object of one of the child classes depending on the argument.

For example, guess our child classes are Point, Line, and Plane, all are inherited from Base, and the difference between them is set by the dim attribute of the Base class.

class Base():
    
    def __init__(self, dim, a, b):
        self.dim = dim
        self.a = a
        self.b = b
        
class Point(Base):
    
    def __init__(self, a, b):
        super().__init__(1, a, b)
        
class Line(Base):
    
    def __init__(self, a, b):
        super().__init__(2, a, b)
        
class Plane(Base):
    
    def __init__(self, a, b):
        super().__init__(3, a, b)

If I explicitly create a Point, the object type will be Point:

pointA = Point(0, 0)
type(pointA) # __main__.Point

But if I do the same through the Base constructor, the object will be of class Base:

pointB = Base(1, 0, 0)
type(pointB) # __main__.Base

So I'd like to change this behavior and make the Base constructor return a Point, Line or Plane object if the dim attribute is equal to 1, 2 or 3 respectively. How can I do this?

EDIT: Based on this thread (Improper use of __new__ to generate class instances?) I overrid the Base.__new__() and got the following code:

class Base():
    
    def __new__(cls, a, b, dim):
        if dim == 1:
            return object.__new__(Point)
        elif dim == 2:
            return object.__new__(Line)
        elif dim == 3:
            return object.__new__(Plane)    
        
class Point(Base):
    
    def __init__(self, a, b, dim):
        self.a = a
        self.b = b
        
class Line(Base):
    
    def __init__(self, a, b, dim):
        self.a = a
        self.b = b
        
class Plane(Base):
    
    def __init__(self, a, b, dim):
        self.a = a
        self.b = b

The code above works, but it requires an explicit setting of the dim argument even when I create a new Point instance. A non-identical set of arguments in Base.__new__() and Point.__init__() raises an error. How can I keep this behavior but remove dim from the Point constructor?

Boris Silantev
  • 753
  • 3
  • 13
  • 2
    It's generally expected that `type(MyClass(...)) is MyClass` in Python, better not to violate this unwritten rule. It sounds like you may want a factory function, not a type. – wim Apr 18 '22 at 22:03
  • I understand that what I want may contradict good practice, but I'm still wishing to find a way to do that. And I'd like to keep the different classes so that their constructors could be called directly. Right now, when I need to create an object inside a code, I call a function that checks `dim` and either calls one of the subclasses constructors or raises an error (`if not (dim in [1,2,3])`). I'd like to generalize the solution and ban the creation of the Base objects even if the Base constructor is called. – Boris Silantev Apr 18 '22 at 22:51
  • @wim: I beg to differ, there is not such rule. – martineau Apr 18 '22 at 22:59
  • @martineau The datamodel docs say the return value of `object.__new__(cls[, ...])` should _usually_ be an instance of `cls`. Deviating from this has often been a design flaw. To name a couple: [bpo40185](https://bugs.python.org/issue40185) / [GH-19371](https://github.com/python/cpython/pull/19371) refactored it out of `typing.NamedTuple` for Python 3.9, and [bpo24132](https://bugs.python.org/issue24132) has some ongoing efforts to allow `pathlib.Path` subclasses for Python 3.11. I'd caution against `__new__` returning a different type, unless there's an extremely convincing reason to do so. – wim Apr 19 '22 at 01:25
  • 1
    @wim: Deviating from what is commonly done is not automatically a design flaw. Nor does having the `new()` method of the class return a subclass violate [Liskov's substitution principle](https://en.wikipedia.org/wiki/Liskov_substitution_principle). I have successfully used the design pattern in [my answer](https://stackoverflow.com/a/28076300/355230) to the duplicate question numerous times since first learning about it — see my answer to [What exactly is a Class Factory?](https://stackoverflow.com/a/2949205/355230). Like any tool or technique it can be used inappropriately. – martineau Apr 20 '22 at 10:00
  • @martineau: your answer to the duplicate question really helped. However, I still don't understand how to make it work if I want to have a different set of arguments when creating `Base` and `Point` instances. E.g., if I want to exclude `dim` argument from `Point` constructor. Could you please clarify it? – Boris Silantev Apr 20 '22 at 13:26
  • @martineau I didn't say it was automatically a design flaw, and never even mentioned LSP (your logical fallacy is: [strawman](https://yourlogicalfallacyis.com/strawman)). But doing this with the constructors _correctly_ is difficult to implement, and the evidence is there that it can cause problems with inheritance, so a simple factory method is usually a better choice. – wim Apr 20 '22 at 15:06

1 Answers1

1

You don't typically want to do this by instantiating a base case. While I suppose you could override __new__, I wouldn't advise it for this. Notably, though, __init__ has a None return type. So, regardless, you can't do this in __init__ - the object is already created at that point.

Instead, what you probably want is a static so-called 'Factory' method on your base class:

from typing import Type

class Base():

    @staticmethod
    def create(dim, a, b) -> Type[Base]:
        # Decide which subclass you want to create, instantiate and return it.

...
new_obj = Base.create(x, y, z)
Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102