June 15, 2010

Metaclass Madness (Python 3 Version)

Last week I was at PyCon Asia Pacific to deliver the opening keynote. Liew Beng Keat, the conference chair, was kind enough to invite me to give a technical talk as well, so I brushed up a talk that I had given previously to the Icelandic Python User Group entitled Metaclass Madness. The material is fairly straightforward, but metaclasses have the reputation for making people's heads explode, so the title was something of a warning for the unwary.

The evening before the presentation I decided to update the code, so the current download includes not only the PowerPoint slides but also usable source code for both Python 2.x and Python 3.x.

I had thought of adding class decorators to the talk, but interestingly I realized that the example I was using didn't translate. The issue was that the metaclass is called with three arguments, the third of which is a dict containing the namespace that has been constructed during the compilation of the class body. So in the metaclass's __new__() method it is easy to decorate each method by iterating over the namespace dict and replacing each callable (whose name does not begin with a double underscore) with the result of applying a decorator to it.

A class decorator cannot work this way, though (at least with a new-style class, which is all you have in Python 3). The reason is that new-style classes use a dict_proxy object as their __dict__, and the dict_proxy does not all you to set items. Consequently, by the time the decorator gets called the class __dict__ is already pretty much set in concrete.

Since the particular example I chose deliberately omitted the methods whose names began with a double underscore someone asked me whether name mangling would affect the process. [For those who don't know about name mangling it is an attempt to protect "private" variables, those whose names begin with a double underscore and end with at most one underscore. See this documentation page for further details]. I was able to demonstrate interactively, after a couple of false starts, that mangling apparently took place *after* the call to __new__() (presumably in type.__new__(), which the metaclass __new__() method must call to ensure completion of the class creation).


George said...

You can use a class decorator and call setattr() instead of assigning directly to __dict__: http://gist.github.com/440413

Menno Smits said...

Although you can't modify __dict__ directly it appears that setattr still works. Changing the tracing class decorator line:

cdict[n] = traced(cdict[n])


setattr(cls, n, traced(cdict[n]))

allows it to work with new-style classes (including Python 3).

Laurence Rowe said...

From the docs:

Class definitions, like function definitions, may be wrapped by one or more decorator expressions. The evaluation rules for the decorator expressions are the same as for functions. The result must be a class object, which is then bound to the class name.

So your class decorator should be able to create a new class with the required changes to the items in the dict_proxy and return that.


(Disclaimer, I don't use Python 3 yet, so haven't tested this out).

Michael Foord said...

Although the dict proxy object is read only you can still set attributes on the class (having the same effect) - so I don't see why a class decorator shouldn't work. (That's before I've looked at what you're actually trying to do of course... :-)

Steve said...

Thanks, everybody, I figured there had to be a way but I clearly didn't look hard enough.