a nice metaclass example (was Re: structs in python)

Alex Martelli aleax at aleax.it
Sun Jul 7 11:49:16 EDT 2002


Alex Martelli wrote:
        ...
> amount of memory consumed don't interfere.  __slots__ is OK, to save
> memory, when you have many instances with just a few data fields each
> (and the same field names in each instance), but it does admittedly
> demand more sophisticated implementation than Bunch, presumably via a
> metaclass.

Ah, forget the "presumably", here's a simple example.  I've been looking
for good and simple custom metaclass examples for quite a while, after
all, so it would be silly to miss this occasion just because I SHOULD be
finishing the draft of Python in a Nutshell...!-).


import warnings

class metaMetaBunch(type):
    """
    metaclass for new and improved "Bunch": implicitly defines 
    __slots__, __init__ and __repr__ from variables bound in class scope.

    An instance of metaMetaBunch (a class whose metaclass is metaMetaBunch)
    defines only class-scope variables (and possibly special methods, but
    NOT __init__ and __repr__!).  metaMetaBunch removes those variables from
    class scope, snuggles them instead as items in a class-scope dict named
    __dflts__, and puts in the class a __slots__ listing those variables'
    names, an __init__ that takes as optional keyword arguments each of
    them (using the values in __dflts__ as defaults for missing ones), and
    a __repr__ that shows the repr of each attribute that differs from its
    default value (the output of __repr__ can be passed to __eval__ to make
    an equal instance, as per the usual convention in the matter).
    """

    def __new__(cls, classname, bases, classdict):
        """ Everything needs to be done in __new__, since type.__new__ is
            where __slots__ are taken into account.
        """

        # define as local functions the __init__ and __repr__ that we'll
        # use in the new class

        def __init__(self, **kw):
            """ Simplistic __init__: first set all attributes to default
                values, then override those explicitly passed in kw.
            """
            for k in self.__dflts__: setattr(self, k, self.__dflts__[k])
            for k in kw: setattr(self, k, kw[k])

        def __repr__(self):
            """ Clever __repr__: show only attributes that differ from the
                respective default values, for compactness.
            """
            rep = [ '%s=%r' % (k, getattr(self, k)) for k in self.__dflts__
                    if getattr(self, k) != self.__dflts__[k]
                  ]
            return '%s(%s)' % (classname, ', '.join(rep))

        # build the newdict that we'll use as class-dict for the new class
        newdict = { '__slots__':[], '__dflts__':{},
            '__init__':__init__, '__repr__':__repr__, }

        for k in classdict:
            if k.startswith('__'):
                # special methods &c: copy to newdict, warn about conflicts
                if k in newdict:
                    warnings.warn("Can't set attr %r in bunch-class %r" % (
                        k, classname))
                else:
                    newdict[k] = classdict[k]
            else:
                # class variables, store name in __slots__ and name and
                # value as an item in __dflts__
                newdict['__slots__'].append(k)
                newdict['__dflts__'][k] = classdict[k]

        # finally delegate the rest of the work to type.__new__
        return type.__new__(cls, classname, bases, newdict)


class MetaBunch(object):
    """ For convenience: inheriting from MetaBunch can be used to get
        the new metaclass (same as defining __metaclass__ yourself).
    """
    __metaclass__ = metaMetaBunch


# Example use: a meta-bunch class
class Point(MetaBunch):
    """ A point has x and y coordinates, defaulting to 0.0, and a color,
        defaulting to 'gray' -- and nothing more, except what Python and
        the metaclass conspire to add, such as __init__ and __repr__
    """
    x = 0.0
    y = 0.0
    color = 'gray'

# example uses of class Point
q = Point()
print q

p = Point(x=1.2, y=3.4)
print p


Apart from arbitrary ordering of the arguments, and representation of
floating point numbers being not all that nice, you'll see the "print"
statements emit basically the same constructors we just used.


The point is: if you use a lot of "struct-like" classes, metaMetaBunch
offers you a nice and convenient way to define them, with clean syntax
and reasonable default semantics (__init__ and __repr__).  If your
application needs are slightly different, you can of course tweak this
pretty easily -- it IS, after all, a modest amount of pretty simple
lines of code.  An instance of a metabunch class is quite memory-lean,
and using it should be just about as fast as using an instance of the
original Bunch class.  I hope this provides a reasonable starting point
in the study of "what are Python 2.2 metaclasses good for"!

BTW, note that this example doesn't care about the bugfix in the
current CVS 2.3 wrt 2.2 regarding the mangling of slotnames that start
with '__' -- the metabunch class we build doesn't end up with any slotname 
with two leading underscores, anyway, and if we have something like:

class thing(MetaBunch):
    __var = 23

the class-scope variable name is already mangled to _thing__var by
the time our metaMetaBunch class gets to deal with it.  Of course,
this doesn't necessarily make much sense for a class meant to have
no methods (!), but, hey, whatever floats your boat...


Alex





More information about the Python-list mailing list