challenging (?) metaclass problem

Peter Otten __peter__ at web.de
Mon Apr 10 17:11:07 EDT 2006


alainpoint at yahoo.fr wrote:

> I have what in my eyes seems a challenging problem.
> Thanks to Peter Otten, i got the following code to work. It is a sort
> of named tuple.

Don't trust code posted in a newsgroup -- it may sometimes be
quality-challenged :-)

> Now my problem is the following. I want to write a function called
> createDerivedTuple in order to create a class derived from the one
> created with the function createTuple, taking the parent class as first
> argument:
> def createDerivedTuple(parentclass,*names):
> ....... code yet to be figured out ....
> 
> The usage should be as follows:
> 
> DerivedPoint=makeDerivedTuple(Point,'z')
> p=DerivedPoint(4,7,9)
> assert p.x==p[0]
> assert p.y==p[1]
> assert p.z==p[2]
> assert DerivedPoint.x==0
> assert DerivedPoint.y==1
> assert DerivedPoint.z==2
> 
> I am still a newbie on metaclasses but i am convinced there are elegant
> solutions to this challenging problem.

Hey, if you don't try to solve it yourself we'll end up with me learning
more than you...

I didn't find a way to extend the approach with a per-class metaclass to
support inheritance. I think you would have to subclass both class and
metaclass. So I had to start all over again:

class IndexProperty(object):
    def __init__(self, index):
        self.index = index
    def __get__(self, inst, cls):
        index = self.index
        if inst is None:
            return index
        return inst[index]

class TupleType(type):
    def __new__(mcl, name, bases, classdict):
        names = classdict.get("_names")
        if names is not None:
            for base in bases:
                base_names = getattr(base, "_names", None)
                if base_names is not None:
                    offset = len(base_names)
                    break
            else:
                offset = 0
            for i, n in enumerate(names):
                classdict[n] = IndexProperty(offset + i)
            if offset:
                names[:0] = base_names
        return type.__new__(mcl, name, bases, classdict)

class Tuple(tuple):
    __metaclass__ = TupleType
    def __new__(cls, values):
        self = tuple.__new__(cls, values)
        if len(self) != len(cls._names):
            raise TypeError
        return self

import unittest

class P2(Tuple):
    _names = ["x", "y"]

class P3(P2):
    _names = ["z"]

def yield_them(seq):
    """Helper to ensure the implementation cannot rely on len()."""
    for item in seq:
        yield item

class Test(unittest.TestCase):
    def test_P2(self):
        self.assertEquals(P2.x, 0)
        self.assertEquals(P2.y, 1)
        self.assertRaises(AttributeError, lambda: P2.z)

    def test_P3(self):
        self.assertEquals(P3.x, 0)
        self.assertEquals(P3.y, 1)
        self.assertEquals(P3.z, 2)

    def test_P2_inst(self):
        self.assertRaises(TypeError, P2, yield_them("A"))
        self.assertRaises(TypeError, P2, yield_them("ABC"))

        p = P2(yield_them("AB"))
        self.assertEquals(p.x, "A")
        self.assertEquals(p.y, "B")
        self.assertRaises(AttributeError, lambda: p.z)

    def test_P3_inst(self):
        self.assertRaises(TypeError, P3, yield_them("A"))
        self.assertRaises(TypeError, P3, yield_them("ABCD"))

        p = P3(yield_them("ABC"))
        self.assertEquals(p.x, "A")
        self.assertEquals(p.y, "B")
        self.assertEquals(p.z, "C")

        self.assertRaises(AttributeError, lambda: p.t)


if __name__ == "__main__":
    unittest.main()

I did not perform any tests other than the unittest given above.

Peter



More information about the Python-list mailing list