views:

70

answers:

1

I want to have immutable types that can, ideally, sort out their own hashing and equality, but can be easily subclassed. I started off using namedtuple:

class Command(namedtuple('Command', 'cmd_string')):

  def valid_msg(msg):
    return True

  def make_command(msg):
    if self.valid_msg(msg):
      return '%s:%s' % (self.cmd_string, msg)
    else:
      raise ValueError(INVALID_MSG)

...but this does not lend itself to subclassing. Directly subclassing this means that the name of the tuple remains the same (for printing... not such a big deal), but more importantly you can't add fields:

class LimitedLengthCommand(Command):

  # I want to have self.length! Where does it go?

  def valid_msg(msg):
    return len(msg) <= self.length

Simply creating another named tuple (as per the docs) means I don't inherit any methods!

What is the simplest and easiest way to do something like this? I intend to have multiple subclasses of Command (eg. hex literals, 1-or-0, etc), but nothing complicated. Playing nice with multiple inheritance is not essential.

+1  A: 

Here's a metaclass to do what you want (I think). It works by storing the methods to be inherited in a dictionary and manually inserting them into the new classes dictionary. It also stores the attribute string that gets passed to the namedtuple constructor and merges that with the attribute string from the subclass. It then passes that to namedtuple and returns a class that's inherited from the resulting namedtuple with all appropriate methods in its dictionary. Because the metaclass is derived from abc.ABCMeta, you get working type checking for free. Here's how constructing a couple of classes looks:

class Foo(object):
    __metaclass__ = ImmutableMeta
    _attributes_ = 'a b'

    def sayhi(self):
        print "Hello from {0}".format(type(self).__name__)

class Bar(Foo):
    _attributes_ = 'c'

    def saybye(self):
        print "Goodbye from {0}".format(type(self).__name__)

Here's the metaclass:

import collections as co
import abc

class ImmutableMeta(abc.ABCMeta):

    _classes = {}

    def __new__(meta, clsname, bases, clsdict):
        attributes = clsdict.pop('_attributes_')

        if bases[0] is object:
            # 'new' class
            methods = clsdict
        else:
            # we're 'inheriting' from an existing class
            base = bases[0]
            attributes = meta._classes[base]['attributes'] + ' ' + attributes
            base_methods = meta._classes[base]['methods'].copy()
            base_methods.update(clsdict)
            methods = base_methods

        # construct the actual base class and create the return class
        new_base = co.namedtuple(clsname + 'Base', attributes)
        cls = super(ImmutableMeta, meta).__new__(meta, clsname, (new_base,),
                                                 methods)

        # register the data necessary to 'inherit' from the class
        # and make sure that it passes typechecking
        meta._classes[cls] = {'attributes': attributes,
                              'methods': methods}
        if bases[0] is not object:
            base.register(cls)
        return cls

And here's some paltry test code.

a = Foo(1, 2)
a.sayhi()

b = Bar(1, 2, 3)
b.sayhi()  # 'inherited' from class Foo
b.saybye()

try:
    b.c = 1         # will raise an AttributeError
except AttributeError:
    print "Immutable"

print "issubclass(Bar, Foo): {0}".format(issubclass(Bar, Foo))

try:
   d =  {b: 1}        # No problems
except TypeError:
    print "Cant put it in a dict"
else:
    print "Can put it in a dict"

Hope that helps. If you would prefer not to attach every method to every class that is supposed to inherit it, you could also provide a default __getattr__ that looks through the metaclasses dictionary and finds the appropriate method out of that. That would require somehow hardcoding the baseclass into method, probably using a closure.

aaronasterling
Sorry, my time has been hijacked, so give me a couple of days and I'll try it out. But it looks like a good solution :)
detly