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

Yet another look at Raymond Hettinger's excellent "namedtuple" factory. Unlike Raymond's version, this one minimizes the use of exec. In the original, the entire inner class is dynamically generated then exec'ed, leading to the bulk of the code being inside a giant string template. This version uses a regular inner class for everything except the __new__ method.

Python, 207 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
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# collections.namedtuple, backported to Python 2.4, with a twist.
#
# This should be tested against Python 2.4 through 2.7, but is not expected
# to work in Python 3.


from operator import itemgetter as _itemgetter
from keyword import iskeyword as _iskeyword
import sys as _sys

try:
    all, any
except NameError:
    # Only needed in Python 2.4.
    def all(iterable):
        for element in iterable:
            if not element:
                return False
        return True

    def any(iterable):
        for element in iterable:
            if element:
                return True
        return False


def _check_name(name):
    """Check type or field name is valid.

    Returns an error message if name is not valid, otherwise the
    empty string.
    """
    if not name:
        err = "names must not be empty"
    elif name[0].isdigit():
        err = "name '%s' cannot start with a digit" % name
    elif name.startswith('_'):
        err = "name '%s' cannot start with an underscore" % name
    elif not all(c.isalnum() or c == '_' for c in name):
        err = ("name '%s' must only contain alphanumeric characters"
               " and underscores" % name)
    elif _iskeyword(name):
        err = "name '%s' is a reserved keyword" % name
    else:
        err = ""
    return err


def namedtuple(typename, field_names, verbose=False, rename=False):
    """Returns a new subclass of tuple with named fields.

    >>> Point = namedtuple('Point', 'x y')
    >>> Point.__doc__                   # docstring for the new class
    'Point(x, y)'
    >>> p = Point(11, y=22)             # instantiate with positional args or keywords
    >>> p[0] + p[1]                     # indexable like a plain tuple
    33
    >>> x, y = p                        # unpack like a regular tuple
    >>> x, y
    (11, 22)
    >>> p.x + p.y                       # fields also accessable by name
    33
    >>> d = p._asdict()                 # convert to a dictionary
    >>> d['x']
    11
    >>> Point(**d)                      # convert from a dictionary
    Point(x=11, y=22)
    >>> p._replace(x=100)               # _replace() is like str.replace() but targets named fields
    Point(x=100, y=22)

    """
    if not isinstance(typename, basestring):
        raise TypeError('typename must be a string, not %r' % type(typename))
    err = _check_name(typename)
    if err:
        raise ValueError(err)

    # Parse and validate the field names. Validation serves two purposes,
    # generating informative error messages and preventing template
    # injection attacks.
    if isinstance(field_names, basestring):
        # Field names separated by whitespace and/or commas.
        field_names = field_names.replace(',', ' ').split()
    field_names = list(map(str, field_names))
    seen = set()
    for i, name in enumerate(field_names):
        err = _check_name(name)
        if not err and name in seen:
            err = "duplicate name '%s'" % name
        if err:
            if rename:
                field_names[i] = "_%d" % i
            else:
                raise ValueError(err)
        else:
            seen.add(name)
    field_names = tuple(field_names)

    # === Dynamically construct the class ===

    # Unlike Raymond Hettinger's original recipe found at
    # http://code.activestate.com/recipes/500261-named-tuples/
    # we use a regular nested class. The only method which needs to be
    # generated dynamically is __new__.

    numfields = len(field_names)
    reprtxt = ', '.join('%s=%%r' % name for name in field_names)
    argtxt = ', '.join(field_names)

    class Inner(tuple):
        # Work around for annoyance: type __doc__ is read-only :-(
        __doc__ = ("%(typename)s(%(argtxt)s)"
                   % {'typename': typename, 'argtxt': argtxt})

        __slots__ = ()
        _fields = field_names

        # Don't decorate with classmethod here. See below.
        def _make(cls, iterable, new=tuple.__new__, len=len):
            """Make a new %s object from a sequence or iterable."""
            result = new(cls, iterable)
            if len(result) != numfields:
                raise TypeError('Expected %d arguments, got %d' % (numfields, len(result)))
            return result

        # Work around for annoyance: classmethod __doc__ is read-only :-(
        _make.__doc__ %= locals()
        _make = classmethod(_make)

        def __repr__(self):
            return '%s(%s)' % (typename, reprtxt%self)

        def _asdict(self):
            """Return a new dict which maps field names to their values."""
            return dict(zip(self._fields, self))

        def _replace(self, **kwds):
            """Return a new %(typename)s object replacing specified fields with new values."""
            result = self._make(map(kwds.pop, self._fields, self))
            #result = self._make(map(kwds.pop, ('x', 'y'), _self))
            if kwds:
                raise ValueError('Got unexpected field names: %r' % kwds.keys())
            return result

        def __getnewargs__(self):
            return tuple(self)

    # For pickling to work, the __module__ attribute needs to be set to the
    # frame where the named tuple is created.  Bypass this step in enviroments
    # where sys._getframe is not defined (Jython for example) or sys._getframe
    # is not defined for arguments greater than 0 (IronPython).
    try:
        Inner.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__')
    except (AttributeError, ValueError):
        pass

    # Dynamically create the __new__ method and inject it into the class.
    # We do this using exec because the method argument handling is otherwise
    # too hard. The "cls" parameter is named _cls instead to avoid clashing
    # with a field of that same name.
    ns = {'_new': tuple.__new__}
    template = """def __new__(_cls, %(argtxt)s):
        return _new(_cls, (%(argtxt)s))""" % locals()
    if verbose:
        print template
    exec template in ns, ns
    Inner.__new__ = staticmethod(ns['__new__'])  # NOT classmethod!

    # Inject properties to retrieve items by name.
    for i, name in enumerate(field_names):
        setattr(Inner, name, property(_itemgetter(i)))

    Inner.__dict__['_replace'].__doc__ %= locals()
    Inner.__name__ = typename
    return Inner


if __name__ == '__main__':
    # verify that instances can be pickled
    from cPickle import loads, dumps
    Point = namedtuple('Point', 'x, y', True)
    p = Point(x=10, y=20)
    assert p == loads(dumps(p, -1))

    # test and demonstrate ability to override methods
    class Point(namedtuple('Point', 'x y')):
        @property
        def hypot(self):
            return (self.x ** 2 + self.y ** 2) ** 0.5
        def __str__(self):
            return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot)

    for p in Point(3,4), Point(14,5), Point(9./7,6):
        print (p)

    class Point(namedtuple('Point', 'x y')):
        'Point class with optimized _make() and _replace() without error-checking'
        _make = classmethod(tuple.__new__)
        def _replace(self, _map=map, **kwds):
            return self._make(_map(kwds.get, ('x', 'y'), self))

    print Point(11, 22)._replace(x=100)

    import doctest
    TestResults = namedtuple('TestResults', 'failed attempted')
    print TestResults(*doctest.testmod())