In this tutorial I demonstrate some examples to implement class factories using Python programming language. Examples are based on Python 2.7
You may also read my metaclass tutorial
In this example, lets build a class factory. We have two different Connection
classes, one for mysql based connections called ConnectionMysql
and one for sqlite based connections called ConnectionSqlite
for our virtual application. They have same methods but working differently.
class CursorMysql(object): pass class CursorSqlite(object): pass class ConnectionBase(object): '''This class is our base connection class defining the methods''' def cursor(self): '''override this''' class ConnectionMysql(ConnectionBase): def __init__(self, username, password, host): '''do some mysql stuff here''' def cursor(self): ''' do some mysql stuff here ''' return CursorMysql() class ConnectionSqlite(ConnectionBase): def __init__(self, path): '''do some sqlite stuff here''' def cursor(self): ''' do some sqlite stuff here ''' return CursorSqlite() def connection(username=None, password=None, host=None, path=None, db="mysql"): ''' This is our factory function which returns appropriate instance according to the arguments given and it also asserts the inputs based on the db parameter ''' if db == "sqlite": # if db is sqlite, other than path parameter rest must be None assert username is None and password is None and path is not None return ConnectionSqlite(path=path) # return sqlite connection object elif db == "mysql": # if db is mysql, only path must be None that is sqlite specific argument assert path is None and username is not None and password is not None # return mysql connection object return ConnectionMysql(username=username, password=password, host=host) else: raise Exception("Unsupported database") conn = connection(path="db.sqlite", db="sqlite") print conn # <__main__.ConnectionSqlite object at 0x0239BC10> print type(conn) # <class '__main__.ConnectionSqlite'> conn2 = connection(username="xyz", password="secret", host="192.168.1.10", db="mysql") print conn2 # <__main__.ConnectionMysql object at 0x0230BCB0> print type(conn2) # <class '__main__.ConnectionMysql'>
As can been from the outputs above, factory function returns required instances. You can put classes in factory functions if you want to hide them.
Now, lets follow a different way and use magical method __new__
to implement a new factory but this time we use class to return the correct connection instance. Bear in my that above approach is better than using __new__
as factory.
To understand how __new__
works, here is a small example;
class Connection(object): def __new__(cls, *args, **kwargs): print cls, args, kwargs return object.__new__(cls, *args, **kwargs) def __init__(self, *args, **kwargs): pass print Connection(1, a="a") # output is # <class '__main__.Connection'> (1,) {'a': 'a'} # <__main__.Connection object at 0x023ABD70>
The output shows that __new__
method runs before __init__
.
First argument of __new__
is cls
not self
. Other arguments are the same what __init__
takes.
Let's see what Python 2 documentation says about __new__
object.__new__(cls[, ...])
Called to create a new instance of class cls. __new__()
is a static method (special-cased so you need not declare it as such) that takes the class of which an instance was requested as its first argument. The remaining arguments are those passed to the object constructor expression (the call to the class). The return value of __new__()
should be the new object instance (usually an instance of cls).
Typical implementations create a new instance of the class by invoking the superclass’s __new__()
method using super(currentclass, cls).__new__(cls[, ...])
with appropriate arguments and then modifying the newly-created instance as necessary before returning it.
If __new__()
returns an instance of cls, then the new instance’s __init__()
method will be invoked like __init__(self[, ...])
, where self is the new instance and the remaining arguments are the same as were passed to __new__()
.
If __new__()
does not return an instance of cls, then the new instance’s __init__()
method will not be invoked.
__new__()
is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation. It is also commonly overridden in custom metaclasses in order to customize class creation.
As the official documentation tells us that, __new__
is responsible for creation of the instance and __init__
is responsible for initializing the instance. So, we can hack the __new__
to return different instance that the class it is in. I use hack
term beacuse __new__
is not intended to do this.
Here is the factory class. I didn't put the whole example, you can copy it from above.
class Connection(object): def __new__(cls, username=None, password=None, host=None, path=None, db="mysql"): if db == "sqlite": # if db is sqlite, other than path parameter rest must be None assert username is None and password is None and path is not None # return sqlite connection object return ConnectionSqlite.__new__(ConnectionSqlite, path=path) elif db == "mysql": # if db is mysql, only path must be None that is sqlite specific argument assert path is None and username is not None and password is not None # return mysql connection object return ConnectionMysql.__new__(ConnectionMysql, username=username, password=password, host=host) else: raise Exception("Unsupported database") conn = Connection(path="db.sqlite", db="sqlite") print conn # <__main__.ConnectionSqlite object at 0x0250BDD0> print type(conn) # <class '__main__.ConnectionSqlite'> conn2 = Connection(username="xyz", password="secret", host="192.168.1.10", db="mysql") print conn2 # <__main__.ConnectionMysql object at 0x0250BDF0> print type(conn2) # <class '__main__.ConnectionMysql'>
Output is as expected so it works as our factory class. This demonstration is just for showing how __new__
method works so I do not prefer to use it as a factory.
This is an easy solution. Use a dictionary holding the classes and return the appropriate class as can be seen from the example below.
conn_dict = {"mysql": ConnectionMysql, "sqlite": ConnectionSqlite} connection = conn_dict["mysql"](username="username", password="password", host="192.168.1.10")
Discussion