ActiveState Code

Recipe 303481: Tuples with named elements - using metaclasses


NamedTupleMetaclass is a metaclass for creating tuples with named elements that can be accessed by index and by name.

NamedTuple is a class factory for NamedTupleMetaclass instances.

This is an improved version of recipe #303439

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class NamedTupleMetaclass(type):
    """Metaclass for a tuple with elements named and indexed.
    
    NamedTupleMetaclass instances must set the 'names' class attribute
    with a list of strings of valid identifiers, being the names for the
    elements. The elements can then be obtained by looking up the name or the index.
    """

    def __init__(cls, classname, bases, classdict):
        super(NamedTupleMetaclass, cls).__init__(cls, classname, bases, classdict)

        # Must derive from tuple
        if not tuple in bases:
            raise ValueError, "'%s' must derive from tuple type." % classname
            
        # Create a dictionary to keep track of name->index correspondence
        cls._nameindices = dict(zip(classdict['names'], range(len(classdict['names']))))
        
        
        def instance_getattr(self, name):
            """Look up a named element."""
            try:
                return self[self.__class__._nameindices[name]]
            except KeyError:
                raise AttributeError, "object has no attribute named '%s'" % name

        cls.__getattr__ = instance_getattr

        
        def instance_setattr(self, name, value):
            raise TypeError, "'%s' object has only read-only attributes (assign to .%s)" % (self.__class__.__name__, name)

        cls.__setattr__ = instance_setattr

        
        def instance_new(cls, seq_or_dict):
            """Accept either a sequence of values or a dict as parameters."""
            if isinstance(seq_or_dict, dict):
                seq = []
                for name in cls.names:
                    try:
                        seq.append(seq_or_dict[name])
                    except KeyError:
                        raise KeyError, "'%s' element of '%s' not given" % (name, cls.__name__)
            else:
                seq = seq_or_dict
            return tuple.__new__(cls, seq)

        cls.__new__ = staticmethod(instance_new)


def NamedTuple(*namelist):
    """Class factory function for creating named tuples."""
    class _NamedTuple(tuple):
        __metaclass__ = NamedTupleMetaclass
        names = list(namelist)

    return _NamedTuple


# Example follows
if __name__ == "__main__":
    class PersonTuple(tuple):
        __metaclass__ = NamedTupleMetaclass
        names = ["name", "age", "height"]

    person1 = PersonTuple(["James", 26, 185])
    person2 = PersonTuple(["Sarah", 24, 170])
    person3 = PersonTuple(dict(name="Tony", age=53, height=192))
    
    print person1
    for i, name in enumerate(PersonTuple.names):
        print name, ":", person2[i]
    print "%s is %s years old and %s cm tall." % person3

    person3.name = "this will fail"

Discussion

A common usage of tuples is to represent aggregations of heterogenous data, as a kind of anonymous data structure. The built-in tuple type only allows the elements to be accessed by their indices; this leads to a loss of clarity: seeing x[3] in code is less clear than x.middlename, for example.

Classes using NamedTupleMetaclass are tuples with named elements. These elements can then be accessed by index or by name, as convenient. See the code for some example usage.

Because NamedTuple is a subclass of tuple, all the standard tuple methods will work on it as usual. This provides the convenience of tuples with the clarity of named elements.

Comments

  1. 1. At 7:09 p.m. on 9 sep 2004, Andrew Durdin (the author) said:

    Not enough examples. The examples of usage should have included using the factory function, and accessing the named members:

    PersonTuple = NamedTuple("name", "age", "height")
    person1 = PersonTuple(["James", "25", "185"])
    print person1.name, person1.age, person1.height
    
  2. 2. At 7:37 p.m. on 9 sep 2004, Andrew Durdin (the author) said:

    Great speed improvement. It struck me that, since the members of the tuple are immutable, the best implementation would just keep another reference to them for each attribute. This not only ends up being simpler, but from my calculations about 1400% faster -- a very significant increase:

    class NamedTupleMetaclass(type):
        """Metaclass for a tuple with elements named and indexed.
    
        NamedTupleMetaclass instances must set the 'names' class attribute
        with a list of strings of valid identifiers, being the names for the
        elements. The elements can then be obtained by looking up the name or the index.
        """
    
        def __init__(cls, classname, bases, classdict):
            # Must derive from tuple
            if not tuple in bases:
                raise ValueError, "'%s' must derive from tuple type." % classname
    
            type.__init__(cls, classname, bases, classdict)
    
    
            def instance_setattr(self, name, value):
                raise TypeError, "'%s' object has only read-only attributes (assign to .%s)" % (self.__class__.__name__, name)
    
            cls.__setattr__ = instance_setattr
    
    
            def cls_new(cls, seq_or_dict):
                # Accept either a sequence of values or a dict as parameters.
                if isinstance(seq_or_dict, dict):
                    seq = []
                    for name in cls.names:
                        try:
                            seq.append(seq_or_dict[name])
                        except KeyError:
                            raise KeyError, "'%s' element of '%s' not given" % (name, cls.__name__)
                else:
                    seq = seq_or_dict
    
                instance = tuple.__new__(cls, seq)
    
                # Create attributes corresponding to the names with the same values (it's an immutable object, after all!)
                for i, name in enumerate(cls.names):
                   tuple.__setattr__(instance, name, seq[i])
    
                return instance
    
            cls.__new__ = staticmethod(cls_new)
    
  3. 3. At 3:43 a.m. on 10 sep 2004, Just van Rossum said:
  4. 4. At 5:03 a.m. on 13 sep 2004, Christos Georgiou said:

    Even more prior art. http://www.sil-tec.gr/~tzot/python/ (TupleStruct.py)

    The improvement for speed I have used was to set property gets for the members using the operator.attrgetter function. I didn't set separately members since that would mean extra memory usage.

    Oldest article referencing this module from Dec 27, 2002, in a thread where Alex Martelli was involved too:

    http://groups.google.com/groups?selm=735o0vk4uciifl692o51fk9om87o37bo6k%404ax.com

Sign in to comment