Some Guy
Some Guy

Reputation: 724

Using __new__ to return the correct subclass, preserving changes made to arguments

class FileHandler:
    def __new__(cls, path, *more_args, **kwargs):
        path = do_stuff_to_path(path)
        new_arg = do_more_stuff(path)
        
        if check_format(path) == "video":
            # return VideoHandler(path, new_arg, *more_args, **kwargs)
        elif check_format(path) == "image":
            # return ImageHandler(path, new_arg, *more_args, **kwargs)
        else:
            # just return FileHandler(path, new_arg, *more_args, **kwargs)

    def __init__(path, new_arg, arg1, arg2, kwarg1=None):
        # stuff


class VideoHandler(FileHandler):
    # stuff

class ImageHandler(FileHandler):
    # stuff

FileHandler("path_to_video")  # instance of VideoHandler
FileHandler("path_to_image")  # instance of ImageHandler

How do I make A return the right subclass while at the same time preserving the changes I made to the arguments, or passing new arguments? Every existing question about this was either written a decade ago for python 2 or suggests using a factory function, which is not what I want.

Upvotes: 1

Views: 619

Answers (2)

JSaur
JSaur

Reputation: 43

Even if the question has been marked as answered, here is a way to do nearly what you are looking for. Except getting a FileHandler instance if no case have been matched.

Here is a basic example for anyone interested, where I didn't use the names defined in the question, because I think that it's now useless.

class Foo :
    instantiation = False

    def __new__(cls, *args, mode=None, **kwargs) :
        if Foo.instantiation :
            Foo.instantiation = False
            return super(Foo, cls).__new__(cls)

        Foo.instantiation = True
        match mode :
            case '1' :
                return Bar1.__new__(Foo, *args, **kwargs)
            case '2' :
                return Bar2.__new__(Foo, *args, **kwargs)

    def __init__(self) -> None:
        pass #do some stuff
    

############


class Bar1 (Foo) :
    def __new__ (cls, value) :
        return Foo.__new__(Bar1, value)
    
    def __init__(self, value, mode) -> None:
        Foo.__init__(self)
        self.value = value


class Bar2 (Foo) :
    def __new__ (cls, value) :
        return Foo.__new__(Bar2, value)
    
    def __init__(self, value, mode) -> None:
        Foo.__init__(self)
        self.value = value

Note that in the code above mode is necessarily a keyword argument. However some modifications are possible to make it both keyword and positional (it will depends on the context where it's used).


So now the pros and cons (mostly the cons) of this implementation.

Pros :

  • Useful for typehinting because Bar1 and Bar2 truly derivates from Foo.

Cons :

  • Make your code hard to read (mostly how the instantiation is handled)
  • You must keep mode in Bar1.__init__() and Bar2.__init__() even if it's not used
  • Make your code slower (approximetly 70% slower)
  • Make Foo non-instantiable (I didn't found any way to make it instantiable so I don't think it's possible)

To conclude, it's strange but not impossible.

Upvotes: 1

chepner
chepner

Reputation: 531075

Rather than forcing __new__ to do this, I would define a separate class method for deciding what to do based on the given path:

class FileHandler:    
    def __init__(self, path, new_arg, arg1, arg2, kwarg1=None):
        # stuff

    @classmethod
    def from_path(cls, path, *args, **kwargs):
        path = do_stuff_to_path(path)
        new_arg = do_more_stuff(path)
        if check_format(path) == "video":
             cls = VideoHandler
        elif check_format(path) == "image":
             cls = ImageHandler
        return cls(path, new_arg, *args, **kwargs)

fh1 = FileHandler.from_path("path_to_video")  # instance of VideoHandler
fh2 = FileHandler.from_path("path_to_image")  # instance of ImageHandler

Now when you actually instantiate the object, only the modified arguments are passed to __init__, since you never actually call the selected type with the original arguments.

Upvotes: 2

Related Questions