Using base class constructor as factory in Python?

Question:

I’m using base class constructor as factory and changing class in this constructor/factory to select appropriate class — is this approach is good python practice or there are more elegant ways?

I’ve tried to read help about metaclasses but without big success.

Here example of what I’m doing.

class Project(object):
  "Base class and factory."
  def __init__(self, url):
      if is_url_local(url):
        self.__class__ = ProjectLocal
      else:
        self.__class__ = ProjectRemote
      self.url = url

class ProjectLocal(Project):
  def do_something(self):
    # do the stuff locally in the dir pointed by self.url

class ProjectRemote(Project):
  def do_something(self):
    # do the stuff communicating with remote server pointed by self.url

Having this code I can create the instance of ProjectLocal/ProjectRemote via base class Project:

project = Project('http://example.com')
project.do_something()

I know that alternate way is to using fabric function that will return the class object based on url, then code will looks similar:

def project_factory(url):
      if is_url_local(url):
        return ProjectLocal(url)
      else:
        return ProjectRemote(url)

project = project_factory(url)
project.do_something()

Is my first approach just matter of taste or it has some hidden pitfalls?

Asked By: bialix

||

Answers:

I think the second approach using a factory function is a lot cleaner than making the implementation of your base class depend on its subclasses.

Answered By: unbeknown

I would stick with the factory function approach. It’s very standard python and easy to read and understand. You could make it more generic to handle more options in several ways such as by passing in the discriminator function and a map of results to classes.

If the first example works it’s more by luck than by design. What if you wanted to have an __init__ defined in your subclass?

Answered By: scottynomad

I usually have a seperate factory class to do this. This way you don’t have to use meta classes or assignments to self.__class__

I also try to avoid to put the knowledge about which classes are available for creation into the factory. Rather, I have all the available classes register themselves withe the factory during module import. The give there class and some information about when to select this class to the factory (this could be a name, a regex or a callable (e.g. a class method of the registering class)).

Works very well for me and also implements such things like encapsulation and information hiding.

Answered By: Ber

The following links may be helpful:
http://www.suttoncourtenay.org.uk/duncan/accu/pythonpatterns.html#factory
http://code.activestate.com/recipes/86900/

In addition, as you are using new style classes, using __new__ as the factory function (and not in a base class, a separate class is better) is what is usually done (as far as I know).

A factory function is generally simpler (as other people have already posted)

In addition, it isn’t a good idea to set the __class__ attribute the way you have done.

I hope you find the answer and the links helpful.

All the best.

Answered By: batbrat

You shouldn’t need metaclasses for this. Take a look at the __new__ method. This will allow you to take control of the creation of the object, rather than just the initialisation, and so return an object of your choosing.

class Project(object):
  "Base class and factory."
  def __new__(cls, url):
    if is_url_local(url):
       return super(Project, cls).__new__(ProjectLocal, url) 
    else:
       return super(Project, cls).__new__(ProjectRemote, url) 

  def __init__(self, url):
    self.url = url
Answered By: Brian

Yeah, as mentioned by @scooterXL, factory function is the best approach in that case, but I like to note a case for factories as classmethods.

Consider the following class hierarchy:

class Base(object):

    def __init__(self, config):
        """ Initialize Base object with config as dict."""
        self.config = config

    @classmethod
    def from_file(cls, filename):
        config = read_and_parse_file_with_config(filename)
        return cls(filename)

class ExtendedBase(Base):

    def behaviour(self):
        pass # do something specific to ExtendedBase

Now you can create Base objects from config dict and from config file:

>>> Base({"k": "v"})
>>> Base.from_file("/etc/base/base.conf")

But also, you can do the same with ExtendedBase for free:

>>> ExtendedBase({"k": "v"})
>>> ExtendedBase.from_file("/etc/extended/extended.conf")

So, this classmethod factory can be also considered as auxiliary constructor.

Answered By: andreypopp

Adding to @Brian’s answer, the way __new__ works with *args and **kwargs would be as follows:

class Animal:
    def __new__(cls, subclass: str, name: str, *args, **kwargs):
        if subclass.upper() == 'CAT':
            return super(Animal, cls).__new__(Dog)
        elif subclass.upper() == 'DOG':
            return super(Animal, cls).__new__(Cat)
        raise NotImplementedError(f'Unsupported subclass: "{subclass}"')

class Dog(Animal):
    def __init__(self, name: str, *args, **kwargs):
        self.name = name
        print(f'Created Dog "{self.name}"')

class Cat(Animal):
    def __init__(self, name: str, *args, num_whiskers: int = 5, **kwargs):
        self.name = name
        self.num_whiskers = num_whiskers
        print(f'Created Cat "{self.name}" with {self.num_whiskers} whiskers')

sir_meowsalot = Animal(subclass='Cat', name='Sir Meowsalot')
shadow = Animal(subclass='Dog', name='Shadow')
Answered By: Abhishek Divekar
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.