Class factory in Python

Français   |   Source

Class factories in Python are an elegant way to use design pattern in Python, or more simply said to define generic (or abstract) classes. The main advantage is to be able to decorrelate the objects you play with and their interface, being more flexible than simple inheritage.

On top of that, an elegant way to manipulate class factories is to use decorator. It is therefore an interesting way to apply some pythonic concepts not so easy to get.

The bits of code in this post can be found on GitHub.

Class factory

First thing to do is to define a class that will be our factory, meaning that this class will allow the abstract class to the right implmentation on the fly:

# Factory class
class DataFactory(object):
    data_maker = {}

    # Decorator to register a new class
    @classmethod
    def register_data(cls, typ):
    def wrapper(maker):
            cls.data_maker[typ] = maker
            return cls
        return wrapper

    # Class maker for new classes with explicit error message if the class does not exist
    @classmethod
    def make_data(cls, typ, *args, **kwargs):
        try:
            return cls.data_maker[typ](*args, **kwargs)
        except KeyError:
            print("TypeError\n'" + typ + "' extension does not exist!")

Here are two methods:

  • register_data -- that is a decorator -- will register any new concrete class implementation.
  • make_data will connect the abstract class to the right one. Here, we'll use a criterion to do so (that is a file format; it will be more explicit in the following).

Generic interface

The second step is to define a generic class that will be the user interface. In the end, we'll interact only with this class to address any other implementation:

# User Interface class: generic class the user will use
class UIClass(object):
    """Generic User class:
    Use this class to do stuff on dummy files.

    Usage:
      myobj = UIClass(fname)
      myobj.load()
    With:
      fname: dummy file name
    """
    def __init__(self, fname):
        self.fname = fname
    # List of data we will retrieve from ad hoc classes
        self._data = ['n', 'x', 'y', 'str']

    # Some method that is calling the method that is calling the class maker
    def load(self):
        ext = self.fname.split('.')[-1]
        self.ext = ext
        self._get_data(self.fname, ext)

    # Method that calls the class maker
    def _get_data(self, fname, ext):
        data_reader = DataFactory.make_data(ext)
        data_reader.load(fname=fname)
        # We transparently load attributes from the created class; we get only the ones defined in _d\
ata
        for attr in self._data:
            self.__setattr__(attr, data_reader.__getattribute__(attr))
        # Get extra methods if defined -- note that the same could be used to discover attributes
        # instead of explicitely defining them in __init__
        for attr in dir(data_reader):
            if callable(getattr(data_reader, attr)) and not attr in dir(self):
                self.__setattr__(attr, data_reader.__getattribute__(attr))

Here, the load() method actually calls the hidden _get_data() method that uses the class factory, chosing the right class with the given file format.

The main trick is to get back attributes and methods from underlying classes. There are two ways to do so:

  • either you can define them explicitely in __init__ method. Most of the time, it should not be a problem to do so because you know what is inside your concrete classes.
  • discover them through getattr. This is much more generic and also allows us to have different interfaces for different concrete classes (we'll give an very simple example in the following; from a practical point of view, it may be interesting to use this: let's imagine you are writting a class to manipulate files, you could wish for a bitmap viewer for images (and therefore an associated method) that would make no sense for a binary file).

Generic class

Then, the concrete underlying classes will inherit from a generic class that will be their template:

# Generic class to build the ad hoc classes; we will inherit from this class
class DataReader(object):
    def __init__(self):
    self.n = 0

    # Hidden empty method that will be defined in the ad hoc class
    def _load(self, *args, **kwargs):
        pass

    # Method used to call the hidden method that will be defined in the ad hoc class
    def load(self, fname):
        self.str = "If defined, then it was loaded"
        self._load(fname)

Nothing unusual here, it's pretty much an empty shell.

Concrete classes

As an example, we will define two underlying classes (for file formats .a and .b) :

# Ad hoc class for extension 'a'
@DataFactory.register_data('a')
class ADataReader(DataReader):
    def __init__(self):
    DataReader.__init__(self)
    self.x = 42
    self.y = 42

    # Actual definition of the previously empty method to do stuff
    def _load(self, fname):
    print("fname: {}".format(fname))

# Ad hoc class for extension 'b'
@DataFactory.register_data('b')
class BDataReader(DataReader):
    def __init__(self):
    DataReader.__init__(self)
    self.x = 'test'
    self.y = [i for i in range(10)]

    # Actual definition of the previously empty method to do stuff
    def _load(self, fname):
    print("fname extension: {}".format(fname.split('.')[-1]))

    def do_smthg(self):
    print("Specific method only for BDataReader")

Few points are worth noticing:

  • the use of decorator: each new implementation is registered with register_data decorator, coming from our factory class. It will be this ID that will be then use to connect the abstract layers with the concrete classes on the fly.
  • attributes: they can be completely different and defined in either of the parent classes.
  • methods: may also be different. Note that BDataReader even has one method that does not exist in other classes but that can be used anyway because it will be "discovered" in the abstract class.

And now, let's test

To be sure that every thing is working as intended:

obj_a = UIClass("file.a")
obj_a.load()

obj_b = UIClass("file.b")
obj_b.load()