Search code examples
pythonpython-3.xsubclass

Python: What is the best way to effectively change the subclass of an instance (keeping the original instances' variables)?


I am not a very great or experienced Python coder, and I'm wondering if there's an easy way to do what I need to do. But I haven't been able to find anything on my own because I'm not sure how to phrase the question properly.

The situation I have is more or less as follows:

class Insect:
  def __init__(self, arg1):
    self.arg1 = arg1

  def insect_method(self):
    print('do insect stuff')

class Caterpillar(Insect):
  def __init__(self, arg1):
    super().__init__(arg1)

  def caterpillar_method(self):
    print('do caterpillar stuff')

class Butterfly(Insect):
  def __init__(self, arg1, arg2):
    super().__init__(arg1)
    self.arg2 = arg2

  def butterfly_method(self):
    print('do butterfly stuff')

I'm not actually simulating caterpillars and butterflies, but it's a similar process so I thought it might help make things clearer.

I have a super class 'Insect' which is never created by itself but exists so subclasses can inherit its methods. Most of the time, I am just spawning the subclasses directly and that's it. But I have a special case where the Caterpillar subclass should become an instance of the Butterfly subclass.

I have searched, and been convinced that changing __class__ is probably not what I want to do in the long run. However, I am also not sure if there's an elegant way to do this otherwise?

The only technique I can think of at the moment is to add in a bunch of if statements like this:

class Insect:
  def __init__(self, arg1):
    self.arg1 = arg1

  def insect_method(self):
    print('do insect stuff')

class Caterpillar(Insect):
  def __init__(self, arg1):
    super().__init__(arg1)

  def caterpillar_method(self):
    print('do caterpillar stuff')

class Butterfly(Insect):
  def __init__(self, arg1, arg2, caterpillar=None):
    if not caterpillar:
        super().__init__(arg1)
    else:
        self.arg1 = caterpillar.arg1
    self.arg2 = arg2

  def butterfly_method(self):
    print('do butterfly stuff')

This would work if the actual code was as simple as this, but it's not, so I'd end up having to make a kind of double of the Insect.__init__ method in the Butterfly.__init__ method and then would have to make sure to constantly update both methods whenever changing anything in the Insect.__init__ method.

Is there an elegant Pythonic method to have this kind of functionality without needing to create a very convoluted __init__ method for the Butterfly class?

Many thanks in advance!!!


Solution

  • Instead of putting this logic in your __init__, I'd maybe keep the __init__ simple and put the logic to create a Butterfly from a Caterpillar in a classmethod:

    class Butterfly(Insect):
        def __init__(self, arg1, arg2):
            super().__init__(arg1)
            self.arg2 = arg2
    
        @classmethod
        def from_caterpillar(cls, caterpillar, arg2):
            return cls(caterpillar.arg1, arg2)
    

    Then you might have code that does something like:

    bob = Caterpillar("Bob")
    # metamorphose
    bob = Butterfly.from_caterpillar(bob, "Small")
    

    Note that similar to your original code, this doesn't mutate the original Caterpillar object to make it a Butterfly, it creates a new Butterfly instance (and reassigns bob to it).