1

I am building a tool that takes directories as inputs and performs actions where necessary. These actions vary depeding on certain variables so I created a few class objects which help me with my needs in an organised fashion.

However, I hit a wall figuring out how to best design the following scenario.

For the sake of simplicity, let's assume there are only directories (no files). Also, the below is a heavily simplified example.

I have the following parent class:

# directory.py
from pathlib import Path

class Directory:
    def __init__(self, absolute_path):
        self.path = Path(absolute_path)

    def content(self):
        return [Directory(c) for c in self.path.iterdir()]

So, I have a method in the parent class that returns Directory instances for each directory inside the initial directory in absolute_path

What the above does, is hold all methods that can be performed on all directories. Now, I have a separate class that inherits from the above and adds further methods.

# special_directory.py
from directory import Directory

class SpecialDirectory(Directory):
    def __init__(self, absolute_path):
        super().__init__(absolute_path)

    # More methods

I am using an Object Factory like approach to build one or the other based on a condition like so:

# directory_factory.py
from directory import Directory
from special_directory import SpecialDirectory

def pick(path):
    return SpecialDirectory(path) if 'foo' in path else Directory(path)

So, if 'foo' exists in the path, it should be a SpecialDirectory instead allowing it to do everything Directory does plus more.

The problem I'm facing is with the content() method. Both should be able to do that but I don't want it to be limited to making a list of Directory instances. If any of its content has "foo*", it should be a SpecialDirectory.

Directory doesn't (and shouldn't) know about SpecialDirectory, so I tried importing and using the factory but it complains about some circular import (which makes sense).

I am not particularly stuck as I have come up with a temp fix, but it isn't pretty. So I was hoping I could get some tips as to what would be an effective and clean solution for this specific situation.

martineau
  • 119,623
  • 25
  • 170
  • 301

1 Answers1

1

What you need is sometimes called a "virtual constructor" which is a way to allow subclasses to determine what type of class instance is created when calling the base class constructor. There's no such thing in Python (or C++ for that matter), but you can simulate them. Below is an example of a way of doing this.

Note this code is very similar to what's in my answer to the question titled Improper use of __new__ to generate classes? (which has more information about the technique). Also see the one to What exactly is a Class Factory?

from pathlib import Path


class Directory:

    subclasses = []

    @classmethod
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

    def __init__(self, absolute_path):
        self.path = Path(absolute_path)

    def __new__(cls, path):
        """ Create instance of appropriate subclass. """
        for subclass in cls.subclasses:
            if subclass.pick(path):
                return object.__new__(subclass)
        else:
            return object.__new__(cls)  # Default is this base class.

    def content(self):
        return [Directory(c) for c in self.path.iterdir()]

    def __repr__(self):
        classname = type(self).__name__
        return f'{classname}(path={self.path!r})'

    # More methods
    ...


class SpecialDirectory(Directory):
    def __init__(self, absolute_path):
        super().__init__(absolute_path)

    @classmethod
    def pick(cls, path):
        return 'foo' in str(path)

    # More methods
    ...


if __name__ == '__main__':
    root = './_obj_factory_test'
    d = Directory(root)
    print(d.content())
martineau
  • 119,623
  • 25
  • 170
  • 301
  • Oh, this looks very interesting. Gonna do some further reading. I came across the `__new__` method upon my research but not the subclass magic. I kinda stopped looking into this further because there seems to be some people who disagree with having a class constructor returning an instance of a different type (a child in this case). – AntsInPants Jan 19 '21 at 22:41
  • 1
    The question [Improper use of __new__ to generate classes?](https://stackoverflow.com/questions/28035685/improper-use-of-new-to-generate-classes) may have been asked for the same reason (which I mention I disagree with in my answer to it). The idea of virtual constructors has been around for quite a while, so you should research it, too (because that's what this is — and it's another name for the Object Factory design pattern). – martineau Jan 19 '21 at 23:01
  • 1
    P.S. I got the idea of using `__init_subclass__()` to register subclasses from a section in the [PEP 487 -- Simpler customisation of class creation](https://www.python.org/dev/peps/pep-0487/#subclass-registration) proposal which was implemented in Python 3.6. – martineau Jan 20 '21 at 00:30
  • Awesome, thanks for all the info! It's been very educational! Just one question if you don't mind me asking for future reference. If it happens that I know that there will only be one subclass, is there any reason you would recommend against skipping the whole `subclass` malarky, and in the `__new__` method do the comparison by doing `if SpecialDirectory.pick(path):` directly? *EDIT* I posted the comment right after you threw the last comment which pretty much responded this haha. Thanks again! – AntsInPants Jan 20 '21 at 00:32
  • 1
    The downside to doing that is it violates encapsulation and means the base class knows about its subclasses. If you're going to do that, might as well just create a factory function that knows about all the subclasses. The main point is about the ability to add additional subclasses without needing to modify the base class (or a separate factory function). – martineau Jan 20 '21 at 00:39
  • Of course. I do want to avoid that. I can mark this as answered. Happy with what I learned! Thanks again! – AntsInPants Jan 20 '21 at 01:07