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 [EMAIL PROTECTED] 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/
_______________________________________________
Python-3000 mailing list
[email protected]
http://mail.python.org/mailman/listinfo/python-3000
Unsubscribe: 
http://mail.python.org/mailman/options/python-3000/archive%40mail-archive.com

Reply via email to