Skip to content


Python factory-like type instances

When designing applications or libraries, sometimes you need to be able to create instances of a certain interface (in a liberal sense) at runtime without knowing at write/compile time which specific implementation (class) you’ll need to use, as this could depend on runtime variables.

An example of this is an interface providing some functionality which should be implemented differently on different platforms, eg Linux and Windows.

There are some standard patterns how to achieve this. One of them is the factory pattern, which works somewhat like this Python example (let’s pretend ‘PLATFORM’ is ‘linux2′ or ‘win32′, ie sys.platform):

#Pretend we use sys.platform instead of PLATFORM where we use it
PLATFORM = 'linux2'

class FooBase(object):
    def say_foo(self):
        print 'foo'

class PlatformFoo(FooBase):
    def say_platform_foo(self):
        raise NotImplementedError

    @staticmethod
    def get_class():
        #Several ways to get this (dict, introspection, if-tree,...), pick yours
        klass = {
            'linux2': LinuxFoo,
            'win32': WindowsFoo,
        }.get(PLATFORM, None)
        if not klass:
            raise Exception, 'Platform not supported'
        return klass

class WindowsFoo(PlatformFoo):
    def say_platform_foo(self):
        print 'win32 foo'

class LinuxFoo(PlatformFoo):
    def say_platform_foo(self):
        print 'linux foo'

def main():
    foo_class = PlatformFoo.get_class()
    foo = foo_class()
    foo.say_platform_foo()

if __name__ == '__main__':
    main()

Executing this code will, as expected, write ‘linux foo’ to the console. Obviously we could not return the platform-specific class in a PlatformFoo function, but an actual instance, up to you.

Python allows you to handle this situation somewhat nicer though, without introducing any intermediate functions, by using metaclasses.

I won’t explain what metaclasses are here, or how they work, there are several resources on the internet explaining them. Let’s just get to the code:

#Pretend we use sys.platform instead of PLATFORM where we use it
PLATFORM = 'linux2'

class FooBase(object):
    def say_foo(self):
        print 'foo'

    def say_platform_foo(self):
        raise NotImplementedError

class WindowsFoo(FooBase):
    def say_platform_foo(self):
        print 'win32 foo'
         
class LinuxFoo(FooBase):
    def say_platform_foo(self):
        print 'linux foo'


class FooMeta(type):
    def __new__(cls, name, bases, attrs):
        #Several ways to get this (dict, introspection, if-tree,...), pick yours
        klass = {
            'linux2': LinuxFoo,
            'win32': WindowsFoo,
        }.get(PLATFORM, None)
        if not klass:
            raise Exception, 'Platform not supported'
        return klass

class Foo:
    __metaclass__ = FooMeta


def main():
    foo = Foo()
    foo.say_platform_foo()

if __name__ == '__main__':
    main()

See we don’t need any getter-function here, but we can just create a ‘Foo’ instance? The resulting object will be a ‘LinuxFoo’ or a ‘WindowsFoo’ as expected, depending on the value of ‘PLATFORM’. The above code also displays ‘linux foo’.

There’s something nifty about it too: you don’t loose any class inheritance information:

In [1]: from test2 import Foo, LinuxFoo, FooBase

In [2]: f = Foo()

In [3]: f.say_platform_foo()
linux foo

In [4]: type(f)
Out[4]: 

In [5]: isinstance(f, LinuxFoo)
Out[5]: True

In [6]: isinstance(f, FooBase)
Out[6]: True

In [7]: isinstance(f, Foo)
Out[7]: True

This shouldn’t surprise you, as ‘Foo’ actually became an alias for ‘LinuxFoo’.

Maybe this pattern will be useful in one of your projects one day, who know :-)

Posted in Development, Technology.

Tagged with , .


2 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

  1. Quikee says

    I don’t know.. I prefer the below example mainly because it is without metaclass hackery (Foo magicly has some functionality but does not inherit from anything), but not quite so different:

    import sys
    
    class FooBase(object):
    	def __new__(cls, *arguments, **keyword):
    		for subclass in FooBase.__subclasses__():
    			if subclass.platform == sys.platform:
    				return super(cls, subclass).__new__(subclass, *arguments, **keyword)
    		raise Exception, 'Platform not supported'
    	
    	def say_foo(self):
    		print 'foo'
    
    	def say_platform_foo(self):
    		raise NotImplementedError
    
    class WindowsFoo(FooBase):
    	platform = 'win32'
    	
    	def say_platform_foo(self):
    		print 'win32 foo'
    
    class LinuxFoo(FooBase):
    	platform = 'linux2'
    	
    	def say_platform_foo(self):
    		print 'linux foo'
    
    if __name__ == '__main__':
    	foo = FooBase()
    	foo.say_platform_foo()
    	def say_platform_foo(self):
    		print 'linux foo'
    • Pythongg says

      So if you were to do what you said but instead had WindowsFoo and Linux foo be in two different files, how could you achieve the same result? __subclasses__() is empty if WindowsFoo and LinuxFoo are not in the same file

      I’m asking because I’m trying to implement the same sort of idea but I want my subclasses to be in different files



Some HTML is OK

or, reply to this post via trackback.