'Correct way of returning new class object (which could also be extended)

I am trying to find a good way for returning a (new) class object in class method that can be extended as well.

I have a class (classA) which has among other methods, a method that returns a new classA object after some processing

class classA:
   def __init__(): ...

   def methodX(self, **kwargs):
      process data
      return classA(new params)

Now, I am extending this class to another classB. I need methodX to do the same, but return classB this time, instead of classA

class classB(classA):
   def __init__(self, params):
      super().__init__(params)
      self.newParams = XYZ
   
   def methodX(self, **kwargs):
      ???

This may be something trivial but I simply cannot figure it out. In the end I dont want to rewrite the methodX each time the class gets extended.

Thank you for your time.



Solution 1:[1]

Use the __class__ attribute like this:

class A:
    def __init__(self, **kwargs):
        self.kwargs = kwargs

    def methodX(self, **kwargs):
        #do stuff with kwargs
        return self.__class__(**kwargs)

    def __repr__(self):
        return f'{self.__class__}({self.kwargs})'

class B(A):
    pass

a = A(foo='bar')
ax = a.methodX(gee='whiz')
b = B(yee='haw')
bx = b.methodX(cool='beans')

print(a)
print(ax)
print(b)
print(bx)

Solution 2:[2]

class classA:
    def __init__(self, x):
       self.x = x

    def createNew(self, y):
        t = type(self)
        return t(y)

class classB(classA):
    def __init__(self, params):
        super().__init__(params)


a = classA(1)
newA = a.createNew(2)

b = classB(1)
newB = b.createNew(2)

print(type(newB))
# <class '__main__.classB'>

Solution 3:[3]

I want to propose what I think is the cleanest approach, albeit similar to existing answers. The problem feels like a good fit for a class method:

class A:
    @classmethod
    def method_x(cls, **kwargs):
        return cls(<init params>)

Using the @classmethod decorator ensures that the first input (traditionally named cls) will refer to the Class to which the method belongs, rather than the instance.

(usually we call the first method input self and this refers to the instance to which the method belongs)

Because cls refers to A, rather than an instance of A, we can call cls() as we would call A().

However, in a class that inherits from A, cls will instead refer to the child class, as required:

class A:
    def __init__(self, x):
        self.x = x
    @classmethod
    def make_new(cls, **kwargs):
        y = kwargs["y"]
        return cls(y) # returns A(y) here

class B(A):
    def __init__(self, x):
        super().__init__(x)
        self.z = 3 * x

inst = B(1).make_new(y=7)
print(inst.x, inst.z)

And now you can expect that print statement to produce 7 21.

That inst.z exists should confirm for you that the make_new call (which was only defined on A and inherited unaltered by B) has indeed made an instance of B.


However, there's something I must point out. Inheriting the unaltered make_new method only works because the __init__ method on B has the same call signature as the method on A. If this weren't the case then the call to cls might have had to be altered.

This can be circumvented by allowing **kwargs on the __init__ method and passing generic **kwargs into cls() in the parent class:

class A:
    def __init__(self, **kwargs):
        self.x = kwargs["x"]
    @classmethod
    def make_new(cls, **kwargs):
        return cls(**kwargs)

class B(A):
    def __init__(self, x, w):
        super().__init__(x=x)
        self.w = w
        

inst = B(1,2).make_new(x="spam", w="spam")
print(inst.x, inst.w)

Here we were able to give B a different (more restrictive!) signature.

This illustrates a general principle, which is that parent classes will typically be more abstract/less specific than their children.

It follows that, if you want two classes that substantially share behaviour but which do quite specific different things, it will be better to create three classes: one rather abstract one that defines the behaviour-in-common, and two children that give you the specific behaviours you want.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Kurt
Solution 2 JarroVGIT
Solution 3 Paddy Alton