10

I have a module (db.py) which loads data from different database types (sqlite,mysql etc..) the module contains a class db_loader and subclasses (sqlite_loader,mysql_loader) which inherit from it.

The type of database being used is in a separate params file,

How does the user get the right object back?

i.e how do I do:

loader = db.loader()

Do I use a method called loader in the db.py module or is there a more elegant way whereby a class can pick its own subclass based on a parameter? Is there a standard way to do this kind of thing?

Mike Vella
  • 10,187
  • 14
  • 59
  • 86

4 Answers4

15

Sounds like you want the Factory Pattern. You define a factory method (either in your module, or perhaps in a common parent class for all the objects it can produce) that you pass the parameter to, and it will return an instance of the correct class. In python the problem is a bit simpler than perhaps some of the details on the wikipedia article as your types are dynamic.

class Animal(object):

    @staticmethod
    def get_animal_which_makes_noise(noise):
        if noise == 'meow':
            return Cat()
        elif noise == 'woof':
            return Dog()

class Cat(Animal):
    ...

class Dog(Animal):
    ...
Ian Fiske
  • 10,482
  • 3
  • 21
  • 20
actionshrimp
  • 5,219
  • 3
  • 23
  • 26
  • Thanks! This seems like the most elegant solution! – Mike Vella Sep 02 '11 at 10:25
  • 6
    yea but this would need me to extend the factory method every time I have a new animal.. any way to nicely 'auto-detect' subclasses where `noise` is some attribute value inside the subclasses? – Martin B. Feb 12 '15 at 11:26
  • 2
    You could look up the subclass by that attribute: `return [subclass for subclass in cls.__subclasses__() if subclass.noise == noise][0]` – Frank Niessink Dec 08 '18 at 15:49
  • 1
    I've not seen the factory pattern implemented this way - usually its done in a totally seperate class, or in the factory method pattern, done in individual classes which choose which implementation they need. What is the advantage of doing it in the Parent class itself? Doesn't it break the open-closed principle? – Eoin Oct 11 '22 at 15:56
10

is there a more elegant way whereby a class can pick its own subclass based on a parameter?

You can do this by overriding your base class's __new__ method. This will allow you to simply go loader = db_loader(db_type) and loader will magically be the correct subclass for the database type. This solution is mildly more complicated than the other answers, but IMHO it is surely the most elegant.

In its simplest form:

class Parent():
    def __new__(cls, feature):
        subclass_map = {subclass.feature: subclass for subclass in cls.__subclasses__()}
        subclass = subclass_map[feature]
        instance = super(Parent, subclass).__new__(subclass)
        return instance

class Child1(Parent):
    feature = 1

class Child2(Parent):
    feature = 2

type(Parent(1))  # <class '__main__.Child1'>
type(Parent(2))  # <class '__main__.Child2'>

(Note that as long as __new__ returns an instance of cls, the instance's __init__ method will automatically be called for you.)

This simple version has issues though and would need to be expanded upon and tailored to fit your desired behaviour. Most notably, this is something you'd probably want to address:

Parent(3)  # KeyError
Child1(1)  # KeyError

So I'd recommend either adding cls to subclass_map or using it as the default, like so subclass_map.get(feature, cls). If your base class isn't meant to be instantiated -- maybe it even has abstract methods? -- then I'd recommend giving Parent the metaclass abc.ABCMeta.

If you have grandchild classes too, then I'd recommend putting the gathering of subclasses into a recursive class method that follows each lineage to the end, adding all descendants.

This solution is more beautiful than the factory method pattern IMHO. And unlike some of the other answers, it's self-maintaining because the list of subclasses is created dynamically, instead of being kept in a hardcoded mapping. And this will only instantiate subclasses, unlike one of the other answers, which would instantiate anything in the global namespace matching the given parameter.

ibonyun
  • 425
  • 3
  • 11
  • I really like this. I tried this but I do not get it to work when the chils have some additional parameters/attributes. Do you know how to handle this case? – Patricio Apr 21 '20 at 10:45
  • @Patricio Without seeing your code, I can only guess at what might help... If you have multiple parameters that need to be considered when selecting the subclass, then the keys to `subclass_map` could be tuples of these parameters. – ibonyun Apr 27 '20 at 06:40
  • Basically what I am trying to do is this: https://stackoverflow.com/questions/61339788/dict-attribute-type-to-select-subclass-of-dataclass/61340463#61340463 – Patricio Apr 28 '20 at 18:23
3

I'd store the name of the subclass in the params file, and have a factory method that would instantiate the class given its name:

class loader(object):
  @staticmethod
  def get_loader(name):
    return globals()[name]()

class sqlite_loader(loader): pass

class mysql_loader(loader): pass

print type(loader.get_loader('sqlite_loader'))
print type(loader.get_loader('mysql_loader'))
NPE
  • 486,780
  • 108
  • 951
  • 1,012
  • Thanks! This seems like a great solution, I think essentially the same as actionshrimp? – Mike Vella Sep 02 '11 at 10:26
  • 1
    @Mike Vella: The key distinction from actionshrimp's solution is that here you don't need to explicitly name every class in a series of `if`-`elif` statements. In other words, if you add a new loader class it is instantly available to be specified in the params file: no need to remember to go in and update that big `if`! – NPE Sep 02 '11 at 16:07
2

Store the classes in a dict, instantiate the correct one based on your param:

db_loaders = dict(sqlite=sqlite_loader, mysql=mysql_loader)
loader = db_loaders.get(db_type, default_loader)()

where db_type is the paramter you are switching on, and sqlite_loader and mysql_loader are the "loader" classes.

Roshan Mathews
  • 5,788
  • 2
  • 26
  • 36