Wednesday, April 14, 2010

PHP Factories/Python Factories - Dynamic Class Instantiation

I have been a full-time PHP developer for the last 5 years. Recently, I have begun to branch out my skill-set and learn Python. I struggle with this because there are a lot of the fundamental basics of the language (Python) I do not fully understand. Things you learn in big expensive boring books or through experience.

I am a big fan of the factory design pattern. I use it a lot in PHP, and I find it very useful for initializing things that all use a common interface are base class. See my GeoCoder Extension written for the Yii PHP Framework. I use a factory for loading the driver I want to use.

I was trying to figure out how to do this kind of thing in Python and was struggling. Having been in PHP so long, I am used to a language that is almost as bad as Perl for adding the ability for coders to write Read-only psychotically dynamic code. See PHP Variable variables.

So, after much searching, I found a starting point, and have touched it up to be a little more flexible. The difference in code length will show why I like PHP so much some times. However, the *args and **kwargs do make the Python implementation more powerful for passing arguments to the constructors. In PHP, I just use an init() function and pass an associative array instead.

PHP Code:
/**
* This function does not do file inclusion
* it assumes an autoloader is in place for that
*/
function Factory($class, $options) {
$instance = $class();
$instance->init($options);
return $instance;
}

/********************/
/** USAGE **/
/********************/
class MyClass {
public function __construct() {
}
public function init($args) {
foreach ($args as $key => $value) {
$this->$key = $value;
}
}
}

$instance = Factory('MyClass', array('language' => 'PHP'));


Python Code:
def Factory(fqcn, *args, **kwargs):
"""
This function takes a fully qualified class name (fqcn)
then imports and loads it
If there is no qualifier, or the qualifier is 'global'
then it tries to load the class from globals()
"""
paths = fqcn.split('.')
modulename = '.'.join(paths[:-1])
classname = paths[-1]

if modulename == 'global' or len(modulename) == 0:
klass = globals()[classname]
else:
__import__(modulename)
klass = getattr(sys.modules[modulename], classname)

if len(kwargs) > 0:
return klass(**kwargs)
elif len(args) > 0:
return klass(*args)
else:
return klass()

####################
## USAGE ##
####################

class no_args:
def __init__(self):
pass

inst = Factory('global.no_args')

class two_args:
def __init__(self, arg1, arg2):
self.arg1 = arg1
self.arg2 = arg2

inst = Factory('two_args', 'first', 'second')

class named_args:
def __init__(self, arg1='arg1', arg2='arg2'):
self.arg1 = arg1
self.arg2 = arg2

inst = Factory('named_args', arg2='second')

# Something real
import sys
root = Factory('Tkinter.Tk')
app = Factory('Tkinter.Frame', master=root)



Some people may be thinking "Hey, but what about 'type', isn't that easier to use for this?". In answer to your question: Yes, but No.

Creating a new "instance" of a class with type is very easy. But it does NOT create an instance like most people would think it would. Just try it out, you'll see what I mean.
class myObj(object):
def __init__(self):
print "Object Initialized"

def doAction(self):
print "Object in Action"

real_inst = myObj()
real_inst.doAction()

pseudo_inst = type('myObj', (myObj,), {})()
pseudo_inst.doAction()

dynamic_inst = type('myObj', (object,), {})()
dynamic_inst.doAction()


"But," you say, " pseudo_inst worked just fine!". Well, what is the point of a dynamic factory if you have to have a static class reference in there to make it work? And, thus, the 16 lines of factory goodness were created.

Disclaimer:
I realize the examples here don't require a factory. They are just examples. If you know what factories are and the concepts behind them, you will be able to see where this kind of thing can be useful.

Happy Coding