Class Decorators...

I’m having a great time here at PyCon. Lots of good information all over the place, which is awesome. Add to it the friendliness of everyone, and you really have one of the best conferences ever.

I attended a talk on Class Decorators (PEP 3129), this past Saturday and was very surprised. First, I don’t remember hearing anything about it. Secondly, it had never occurred to me to use decorators to initialize a class–even without the syntactic sugar that is offered by 2.6. Having done a couple of things with metaclasses, I have to say that class decorators are a much better answer.

Here are the key reasons–for me, at least–that makes class decorators The Way To Go:

  • The decorator style is easier to implement.
  • The decorator style is more explicit.
  • Less worries about collisions in attribute names.

To demonstrate the first point, let’s look at the identity versions of a metaclass and a class decorator. First the meta class–straight from Jack Diederich’s slides:

class Identity(type):
    def __new__(meta, name, bases, dict):
        return type.__new__(meta, name, bases, dict)
    def __init__(cls, name, bases, dict):
        pass

Right from the get-go, you can see that there are a few things you need to know about. There are 4 parameters to the __new__ method, and I always get them wrong the first time. On top of that, you have to remember to use the type.__new__ invocation return the new type. Then, you have to actually use it in the class:

class Foo():
    __metaclass__ = Identity
    ...

Now, let’s say that I derive another class from Foo:

class Bar(Foo):
    pass

What’s missing here is that there could be some black-magic going on. What if the metaclass introduced new methods? How do I know where they came from looking at Bar? Simple: I don’t. At least, not without digging into the actual definitions of Foo and Identity.

Let’s take a look at the identity version of the class decorator:

def identity(cls):
    return cls

It’s extremely obvious what this does because of it’s simplicity and it’s directness. Now, let’s use it in Python 2.6 or greater:

@identity
class Foo():
    pass

If you’re using less that Python 2.6, do:

class Foo():
    pass
Foo = identity(Foo)

The decorator version is much simpler than the metaclass version. Additionally, you can see what’s being applied to the class because it’s explicitly marked. On top of that, in order to communicate parameters to the metaclass, you resort to doing things like:

class Bar(Foo):
    __PARAM_NAME__ = "Goober"

If you get several metaclasses into the mix, you could end up with a naming collision and some unexpected behavior. With the decorator version, it would look like:

@mydecorator(paramname="Goober")
class Bar(Foo):
    pass

You don’t risk having a name collision here: paramname is only seen by the decorator, not the actual class. Additionally, you can stack decorators:

@register
@cron.schedule(cron.DAILY)
class Indexer():
    ...

Nice, eh?

As always, there is a trade off. The trade-off here is that class decorators don’t inherit. So, in the above example, if I had a subclass called PdfIndexer, it would not get automatically registered nor would it be scheduled to run daily. Frankly, that’s a trade-off I’m happy to make.

In summary, if you’ve ever thought about using a metaclass, scrap the idea and use a class decorator instead. It’s definitely easier to understand, and easier to implement. Now I have a good reason to choose 2.6 as my default Python installation. :-)