Change the class inside a class with different arguments

Viewed 25

I'm a structured programming guy. So my attempts with object oriented programming are always "work in progress..."

My intent is to have a class which will adapt itself according to an external input. I saw in another post (which I was unable to find again) that I can change the class of an object, so I made this MWE, which works:

class Base:
    def __init__(self, name):
        self.name = name

    def set_text(self, text):
        self.text = text


class Terminator(Base):
    terminator = '!'

    def __init__(self):
        super().__init__('terminator')

    def get(self):
        return self.text + terminator


class Prefix(Base):
    def __init__(self):
        super().__init__('prefix')

    def get(self):
        return str(len(self.text)) + self.text


class_list = {
    'terminator': Terminator,
    'prefix': Prefix
}


class Selector():
    def __init__(self, option):
        self.__class__ = class_list[option]


def main():
    selection = input("Choose 'terminator' or 'prefix': ")
    obj = Selector(selection)
    obj.set_text('something')
    print(obj.get())


if __name__ == '__main__':
    main()

Terminator is a class to produce a text terminated with a special character (!); Prefix produces the same text prefixed with its length.

With Selector, I can use o = Selector('prefix') to get o as a Prefix instance.

The question

My question is if I can add extra arguments to Selector and pass them to the respective class. For example:

o = Selector('prefix', number_of_digits = 2) # '05hello' intead of '5hello'

or

o = Selector('terminator', terminator = '$')  # use '$' instead of '!'

For now, I couldn't figure out how to accomplish this task. I tried to use *args and **kwargs, but unsuccessfully.

Additional information

The code I'm working on is intended to undergraduate students and I want to make it simple for teaching purposes, so Selector should be used to hide other classes and their details from the students (to hide Terminator and Prefix, for example).

I expect to have about 15 distinct classes to hide behind Selector.

Also, I'm ready to hear I'm completely wrong about this approach if there are alternatives.

1 Answers

Try calling the appropriate class's __init__() manually, and set the variables like you otherwise would:

class Terminator(Base):
    # make terminator an instance variable instead of a class variable,
    # and set it as an overridable default arg for the constructor
    def __init__(self, terminator='!'):
        super().__init__('terminator')
        self.terminator = terminator

    def get(self):
        return self.text + self.terminator

class Selector():
    def __init__(self, option, *args, **kwargs):
        self.__class__ = class_list[option]
        self.__class__.__init__(self, *args, **kwargs)

...

o = Selector('terminator', terminator='$')
o.set_text("Hello World")
print(o.get())
# Hello World$

I should leave a disclaimer: what you're trying to do is essentially a version of the Factory method pattern, which is usually easier to maintain if you bundle it into a method instead of messing with class types and reflection:

def Selector(option: str, *args, **kwargs) -> Base:
    return class_list[option](*args, **kwargs)
    # this will do .__new()__ and .__init__() normally, 
    # and is indistinguishable from normal class creation

Using a method to do this instead of overriding the class metadata also has the advantage of being easy to fit into a type system (see the type hinting in the above snippet), which is difficult to do with .__init__(). This is a common design pattern in Java, for example, which is very strongly and statically typed, requires a factory method to have a signature with the superclass of anything it could possibly return, and makes it impossible for an object to change its own type at runtime.

The disadvantage of your current approach, dynamically changing .__class__, is that the .__new__() and .__init__() methods which were called on the resulting object will not match with each other (it would be using Selector.__new__() but Terminator.__init__(), for example), which may cause weird and hard-to-diagnose problems in the future. It's a fun experiment, but be knowledgeable of the risks before using this in something you'll have to maintain for a long time.

Related