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.
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())
|
Tags: shortcuts