Defining a Custom Metaclass to Control Class Behavior

Credit: Luther Blissett

Problem

You want to control the behavior of a class object and all of its instance objects, paying minimal runtime costs.

Solution

Python 2.2 lets you easily define your own custom metaclasses so you can build class objects whose behavior is entirely under your control, without runtime overhead except when the classes are created. For example, if you want to ensure that all methods a class defines (but not those it inherits and doesn’t override) are traced, a custom metaclass that wraps the methods at class-creation time is easy to write:

# requires Python 2.2 or later
import types

def tracing(f, name):
    def traced_f(*a, **k):
        print '%s(%s,%s) ->'%(name,a,k),
        result = f(*a, **k)
        print result
        return result
    return traced_f

class meta_tracer(type):
    def _ _new_ _(self, classname, bases, classdict):
        for f in classdict:
            m = classdict[f]
            if isinstance(m, types.FunctionType):
                classdict[f] = tracing(m, '%s.%s'%(classname,f))
        return type._ _new_ _(self, classname, bases, classdict)

class tracer:
    _ _metaclass_ _ = meta_tracer

Discussion

This recipe’s tracing function is nothing special—it’s just a tracing wrapper closure that makes good use of the lexically nested scopes supported in Python 2.2 (or 2.1 with from _ _future_ _ import nested_scopes). We could use such a wrapper explicitly in each class that needs to be traced. For example:

class prova:
    def a(self):
        print 'body: prova.a'
    a = tracing(a, 'prova.a')
    def b(self):
        print 'body: prova.b'
    b = tracing(b, 'prova.a')

This is okay, but it does require the explicit boilerplate insertion of the decoration (wrapper) around each method we want traced. Boilerplate is boring and therefore error-prone.

Custom metaclasses let us perform such metaprogramming at class-definition time without paying substantial overhead at each instance creation or, worse, at each attribute access. The custom metaclass meta_tracer in this recipe, like most, inherits from type. In our metaclasses, we typically want to tweak just one or a few aspects of behavior, not recode every other aspect, so we delegate all that we don’t explicitly override to type, which is the common metaclass of all built-in types and new-style classes in Python 2.2. meta_tracer overrides just one method, the special method _ _new_ _, which is used to create new instances of the metaclass (i.e., new classes that have meta_tracer as their metaclass). _ _new_ _ receives as arguments the name of the new class, the tuple of its bases, and the dict produced by executing the body of the class statement. In meta_tracer._ _new_ _, we go through this dictionary, ensuring that each function in it is wrapped by our tracing wrapper closure. We then call type._ _new_ _ to do the rest.

That’s all! Every aspect of a class that uses meta_tracer as its metaclass is the same as if it used type instead, except that every method has automagically been wrapped as desired. For example:

class prova(tracer):
    def a(self):
        print 'body: prova.a'
    def b(self):
        print 'body: prova.b'

This is the same as the prova class of the previous snippet, which explicitly wrapped each of its methods. However, the wrapping is done automatically because this prova inherits from tracer and thus gets tracer’s metaclass (i.e., meta_tracer). Instead of using class inheritance, we could control metaclass assignment more explicitly by placing the following statement in the class body:

_ _metaclass_ _ = meta_tracer

Or, more globally, we could place the following statement at the start of the module (thus defining a module-wide global variable named _ _metaclass_ _, which in turn defines the default metaclass for every class that doesn’t inherit or explicitly set a metaclass):

_ _metaclass_ _ = meta_tracer

Each approach has its place in terms of explicitness (always a good trait) versus convenience (sometimes not to be sneered at).

Custom metaclasses also existed in Python Versions 2.1 and earlier, but they were hard to use. (Guido’s essay introducing them is titled “The Killer Joke”, the implication being that those older metaclasses could explode your mind if you thought too hard about them!). Now they’re much simpler thanks to the ability to subclass type and do a few selective overrides, and to the high regularity and uniformity of Python 2.2’s new object model. So there’s no reason to be afraid of them anymore!

See Also

Currently, metaclasses are poorly documented; the most up-to-date documentation is in PEP 253 (http://www.python.org/peps/pep-0253.html).

Get Python Cookbook now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.