Welcome, guest | Sign In | My Account | Store | Cart

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, 76 lines
 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"

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.

4 comments

Andrew Durdin (author) 19 years, 7 months ago  # | flag

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
Andrew Durdin (author) 19 years, 7 months ago  # | flag

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)
Christos Georgiou 19 years, 7 months ago  # | flag

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

Created by Andrew Durdin on Sun, 5 Sep 2004 (PSF)
Python recipes (4591)
Andrew Durdin's recipes (2)

Required Modules

  • (none specified)

Other Information and Tasks