Thursday, March 1, 2007

Python Metaclass on Steroids

In Python Metaclasses are considered an advanced and difficult to use technique. This is certainly true. Developers of 'production code' are often discouraged to use metaclasses, since code that makes use of metaclasses is often overly complex (especially for people who are not used to metaclasses).

A further reason to avoid metaclasses is that they truly can be avoided. They do not add much power to Python. However, there are cases when they make 'client' code more readable.

Consider this situation: we have a finite set of strings that are used as dictionary keys in several parts of our application. Of course, we don't want to use explicit strings in every point where they are needed. This is too error prone and bugs are more difficult to track.

A good solution is to put all the strings in a module. We will have code such as

FOO1 = 'foo1'
FOO2 = 'foo2'

Good. However, we may want to loop over the constants. One of the more pythonic solutions is this one:

targets = {
    'SEARCH': 'search', 'INFO': 'info', 'VARIANTS': 'variants',
    'DEPS': 'deps', 'DEPENDENTS': 'dependents', 
    'INSTALL': 'install','UNINSTALL': 'uninstall',
    'ACTIVATE': 'activate', 'DEACTIVATE': 'deactivate', 
    'INSTALLED':'installed', 'LOCATION': 'location',
    'CONTENTS':'contents', 'PROVIDES': 'provides',
    'SYNC': 'sync', 'OUTDATED': 'outdated',
    'UPGRADE': 'upgrade', 'CLEAN':'clean', 'ECHO': 'echo',
    'LIST': 'list', 'VERSION': 'version', 
    'SELFUPDATE': 'selfupdate', 'HELP': 'help'
}
locals().update(targets)

In this way we can do whatever we want. However, we could use a class instead of a module. We simply define the constants as class variables. Then we can easily use the locals built-in to create the array of constants. But with a little metaclass trick, we can write more interesting client code.
We could for example write things like:

for k in PackageKey:
    # do something

or

if key in PackageKey:
    returnself.foo(key)

We can use metaclasses to define a class object that behaves in the way above.

class_ConstantNamespace(type):
    def__iter__(cls):
        for v incls.keys:
            yield v
    
class PackageKey(object):
    __metaclass__= _ConstantNamespace

NAME                   ='name'
VERSION                ='version'
REVISION               ='revision'
DIRECTORY              ='directory'
VARIANTS               ='variants'
HOMEPAGE               ='homepage'
DESCRIPTION            ='description'
BUILD_DEPENDENCIES     ='build_dependencies'
LIBRARY_DEPENDENCIES   ='library_dependencies'
RUNTIME_DEPENDENCIES   ='runtime_dependencies'
PLATFORMS              ='platforms'
MAINTAINERS            ='maintainers'
keys = [locals()[key] 
        for key inlocals().keys() if (key.upper()== key and 
            (key.isalpha()orkey.endswith('_DEPENDENCIES'))]

No comments: