display | more...

This writeup discusses particularly metaclasses in the Python programming language. They can be quite different in other languages.

Suppose you have a class; let's say it's called Book.

And you know what you can do with that class object? You can call it. And every time you do so, you get an instance of the class — you get a book.

 b1 = Book('Of Mice and Men')
 b2 = Book('Just So Stories')
You could keep calling Book all day, and each time get a Book instance to add to your library. The instances are different because of the different parameters you pass when you call the class.

For ease of illustration, let's see a minimal Book class.
class Book(object):
    def __init__(self, title):
        self.title = title
    def bind(self, method):

Now, let's consider a different class. Different, but same. It's called, and it produces instances. But these instances happen to be classes — this class is a metaclass. Hmmm … what's the point of that? And what would you pass in the call to differentiate the various classes produced, one from the other?

Let's put on our thinking caps and try to figure out, What Would Guido Do? Well, in the normal case, when you call the class, its initializing method (__init__) is called, receiving a new blank instance of the class, and the parameters that were passed to the call. Our metaclass is a class, so it's going to have to work the same way (almost). Its __init__ method will be called and it will receive a new instance (which in this case is a class, because the instances of a metaclass are classes), and just as the normal class initializes the instance, so will the metaclass (almost).

And what parameters will __init__ receive? I don't even recall calling the metaclass, so I don't think I gave it the parameters. There has to be something special about these metaclasses, or C-Dawg wouldn't be writing this. Special in that Python itself makes the call, rather than the programmer. So this is where the slight differences, between "normal" classes and metaclasses, come into play. We're going to say that the metaclass is called as the last step of executing a class statement, and Python passes to it the new class being constructed and various kinds of information about it. Since Python is making the call behind the scenes, we will have no influence on what the call looks like. Guido, pull back the curtain and show us!

class M(object):
    def __init__(icls, classname, bases, namespace):

Okay, __init__ receives the class, the name of the class, a tuple of base classes that the class will inherit from, and a dictionary containing the attributes and methods that we defined. From this we surmise that what the statement class Book(object): does is, saves the tuple of base classes, creates an empty dictionary and populates it with the functions and data objects defined within the scope of the class statement, calls the metaclass passing it those items, and binds the name ("Book") to the object that it returns.

So what? The class is already defined by the time the metaclass sees it (unlike a normal instance, which is empty when the class receives it for initialization); I've already done all the hard work, so what good is the metaclass?

The metaclass can take your already-working class and shape it, polish it, refine it. It can make a variant of it, or it can complete it using what you gave it as boilerplate. But it all happens at the time your class is defined. The metaclass can add or delete attributes or methods, or alter what is there; it cannot alter the base class list, however.

A class defines its metaclass by binding it to a class attribute called __metaclass__. Purists might complain that it should be defined by a syntactical feature, but this way is Pythonic and backward-compatible.

Keep in mind that the metaclass never gets its hands on your class's instances, and especially that your class does not inherit from its metaclass. Also, unlike with inheritance, a class can only have one metaclass. However, since a metaclass is a class, it can inherit from one or more classes. Thus, if you want to use the services of two metaclasses, say MC1 and MC2, all you need to do is define MC3 that inherits from them, and make that your metaclass. And lastly, metaclasses are rarely used for normal application purposes, the default ones that Python supplies being appropriate.

Nitty gritty points that wouldn't be obvious:

  • Your metaclass must subclass type
  • If the metaclass wants to add references (variables or functions) to the class, it must do so using the setattr function. Adding entries to the ns dictionary will not have that effect. ns should be treated as read only.
  • If the class defined __slots__, the metaclass is unable to modify the slots.

I'm going to present an example of a metaclass. It modifies a class so that every entry to and exit from any of its methods is logged. How? For each method that I define in my class, it writes a new function that logs the call, calls my method, logs the return value, and returns that returned value to the caller. The important thing to realize is that this whole process can be turned on and off simply by declaring the class to use the metaclass or not. (And, as seen in the example, the builtin variable __debug__ can be used to control that.)

class MC(type):
    "This metaclass causes all methods of a class to be traced."
    def __init__(cls, clsname, bases, ns):
        def addtrace(f):
            """Returns a function which will trace calls to the
               given function.
            def t(self, *args, **kwargs):
                self.writelog('Enter %s(%s, %s)\n' % (f.__name__, args, kwargs))
                ret = f(self, *args, **kwargs)
                self.writelog('Return %s\n' % `ret`)
                return ret
            return t
        from types import FunctionType
        for name,obj in ns.items():
            if type(obj) == FunctionType:
                setattr(cls, name, addtrace(ns[name]))

class Logger(object):
    def writelog(self, s):
        print s

class C(Logger):
    if __debug__:
        __metaclass__ = MC
    def add(self, a, b):
        return a+b
And here is a sample of it in action:
>>> i = C()
>>> sum = i.add(45, 9)
Enter add((45, 9), {})

Return 54

>>> print sum

Log in or register to write something here or to contact authors.