[Python-3000] PEP Draft: Class Decorators

Jack Diederich jackdied at jackdied.com
Sat Mar 10 01:10:54 CET 2007


I got lots of feedback on the PEP at PyCon and the two main responses
were "heck yeah!" and "what does a class decorator look like?"

I've added some simple examples for the "what is?" and some other 
examples that should help explain the "heck yeah" responses too.

I'm cc'ing peps at python.org for a number assignment (or can I just
check it in myself?)


PEP: 3XXX
Title: Class Decorators
Version: 1
Last-Modified: 28-Feb-2007
Authors: Jack Diederich
Implementation: SF#1671208
Status: Draft
Type: Standards Track
Created: 26-Feb-2007

Abstract
========

Extending the decorator syntax to allow the decoration of classes.

Rationale
=========

Class decorators have an identical signature to function decorators.
The identity decorator is defined as

    def identity(cls):
      return cls

    @identity
    class Example:
        pass

To be useful class decorators should return a class-like thing but
as with function decorators this is not enforced by the language.

All the strong existing use cases are decorators that just register
or annotate classes.

    import myfactory

    @myfactory.register
    class MyClass:
        pass

Decorators are stackable so more than one registration can happen.

    import myfactory
    import mycron

    @mycron.schedule('nightly')
    @myfactory.register
    class MyClass:
        pass

The same effect is currently possible by making a functioon call
after class definition time but as with function decorators decorating
the class moves an important piece of information (i.e. 'this class
participates in a factory') to the top.

Decorators vs Metaclasses
=========================

Decorators are executed once for each time they appear.  Metaclasses
are executed for every class that defines a metaclass and every class
that inherits from a class that defines a metaclass.

Decorators are also easilly stackable because of their takes-a-class
returns-a-class signature.  Metaclasses are combinable too but with
their richer interface it requires more trouble (see Alex Martelli's
PyCon 2005 talk[6]).  Even with some tricks the combined metaclasses
may need to coordinate who owns __new__ or __init__.

Note that class decorators, like metaclasses, aren't garunteed to 
return the same class they were passed.

History and Implementation
==========================

Class decorators were originally proposed alongside function decorators
in PEP318 [1]_ and were rejected by Guido [2]_ for lack of use cases. 
Two years later he saw a use case he liked and gave the go-ahead for a 
PEP and patch [3]_.

The current patch is loosely based on a pre-2.4 patch [4]_ and updated to
use the newer 2.5 (and 3k) AST.

Grammar/Grammar is changed from

   funcdef: [decorators] 'def' NAME parameters ['->' test] ':' suite

to

    decorated_thing: decorators (classdef | funcdef)
    funcdef: 'def' NAME parameters ['->' test] ':' suite

"decorated_thing"s are premitted everywhere that funcdef and classdef
are premitted.

An alternate change to the grammar would be to make a 'decoratable_thing'
which would include funcdefs and classdefs even if they had no decorators.

Motivation
==========

Below is an actual production metaclass followed by its class
decorator equivalent.  It is used to produce factory classes
which each keep track of which account_id web forms are associated
with.

class Register(type):
  """A tiny metaclass to help classes register themselves automagically"""

  def __init__(cls, name, bases, dict):
    if 'DO_NOT_REGISTER' in dict: # this is a non-concrete class
        pass
    elif object not in bases: # this is yet another metaclass
        pass
    elif 'register' in dict: # this is a top level factory class
      setattr(cls, 'register', staticmethod(dict['register']))p
    else: # typical case, register with the factory
      cls.register(name, cls)
    return

class FormFactory(object):
    id_to_form = {} # { account_id : { form_id : form_class } }
    def register(cls):
        FormFactory.id_to_form.setdefault(cls.account_id, {})
        assert cls.form_id not in FormFactory.id_to_form[cls.account_id], cls.form_id
        FormFactory.id_to_form[cls.account_id][cls.form_id] = cls

class FormCaputreA(FormFactory, Form):
    account_id = 17
    form_id = 3
    # .. cgi param to sql mapping defined, etc

The decorated version eliminates the metaclass and loses some of
the if-else checks because it won't be applied by the programmer to
intermediate classes, metaclasses, or factories.

class FormFactory(Form):
    id_to_form = {} # { account_id : { form_id : form_class } }
    @staticmethod
    def register(cls, account_id, form_id):
        FormFactory.id_to_form.setdefault(cls.account_id, {})
        assert form_id not in FormFactory.id_to_form[cls.account_id], form_id
        FormFactory.id_to_form[cls.account_id][form_id] = cls
        # return the class unchanged
        return cls

@FormFactory.register(account_id=17, form_id=3)
class FormCaptureA(object):
    # .. cgi param to sql mapping defined, etc

References
==========
If you enjoyed this PEP you might also enjoy:

.. [1] PEP 318, "Decorators for Functions and Methods"
  http://www.python.org/dev/peps/pep-0318/

.. [2] Class decorators rejection
  http://mail.python.org/pipermail/python-dev/2004-March/043458.html

.. [3] Class decorator go-ahead
  http://mail.python.org/pipermail/python-dev/2006-March/062942.html

.. [4] 2.4 class decorator patch
  http://python.org/sf/1007991

.. [5] 3.x class decorator patch
  http://python.org/sf/1671208

.. [6] Alex Martelli's PyCon 2005 "Python's Black Magic"
  http://www.python.org/pycon/2005/papers/36/


More information about the Python-3000 mailing list