Class factory in Python
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.