Skip to content

Instantly share code, notes, and snippets.

@aliles
Created August 21, 2011 12:06
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aliles/1160525 to your computer and use it in GitHub Desktop.
Save aliles/1160525 to your computer and use it in GitHub Desktop.
Implementation of collections.namedtuple without using exec.
"collections.namedtuple implementation without using exec."
from collections import OrderedDict
from keyword import iskeyword
from operator import itemgetter
import itertools
import sys
__all__ = ['NamedTuple', 'namedtuple']
class NamedTuple(type):
"""Metaclass for a new subclass of tuple with named fields.
>>> class Point(metaclass=NamedTuple):
... _fields = ['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)
"""
def __new__(meta, classname, bases, classdict):
if '_fields' not in classdict:
raise ValueError("NamedTuple must have _fields attribute.")
if tuple not in bases:
bases = tuple(itertools.chain(itertools.repeat(tuple, 1), bases))
for pos, name in enumerate(classdict['_fields']):
classdict[name] = property(itemgetter(pos),
doc='Alias for field number {0:d}'.format(pos))
classdict.update(meta.NAMESPACE)
classdict['__doc__'] = '{0:s}({1:s})'.format(classname,
repr(classdict['_fields']).replace("'", "")[1:-1])
cls = type.__new__(meta, classname, bases, classdict)
cls._make = classmethod(cls._make)
return cls
def _new(cls, *args, **kwargs):
'Create new instance of {0:s}({1:s})'.format(cls.__name__, cls.__doc__)
expected = len(cls._fields)
received = len(args) + len(kwargs)
if received != expected:
raise TypeError('__new__() takes exactly {0:d} arguments ({1:d} given)'.format(received, expected))
values = itertools.chain(args,
(kwargs[name] for name in cls._fields[len(args):]))
return tuple.__new__(cls, values)
def _make(cls, iterable, new=tuple.__new__, len=len):
'Make a new {0:s} object from a sequence or iterable'.format(cls.__name__)
result = new(cls, iterable)
if len(result) != len(cls._fields):
raise TypeError('Expected {0:d} arguments, got {1:d}'.format(
len(cls._fields), len(result)))
return result
def _repr(self):
'Return a nicely formatted representation string'
keywords = ', '.join('{0:s}={1!r:s}'.format(self._fields[i], self[i])
for i in itertools.islice(itertools.count(), len(self._fields)))
classname = self.__class__.__name__
return '{0:s}({1:s})'.format(classname, keywords)
def _asdict(self):
'Return a new OrderedDict which maps field names to their values'
return OrderedDict(zip(self._fields, self))
def _replace(self, **kwargs):
'Return a new {0:s} object replacing specified fields with new values'.format(self.__class__.__name__)
result = self._make(map(kwargs.pop, self._fields, self))
if kwargs:
raise ValueError('Got unexpected field names: {0:r}'.format(kwargs.keys()))
return result
def _getnewargs(self):
'Return self as a plain tuple. Used by copy and pickle.'
return tuple(self)
NAMESPACE = {
'__slots__': (),
'__new__': _new,
'_make': _make,
'__repr__': _repr,
'_asdict': _asdict,
'_replace': _replace,
'__getnewargs__': _getnewargs,
}
def namedtuple(typename, field_names, 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 hasattr(field_names, 'split'):
field_names = field_names.replace(',', ' ').split()
if rename:
names = list(field_names)
seen = set()
for i, name in enumerate(names):
if (not all(c.isalnum() or c == '_' for c in name) or iskeyword(name)
or not name or name[0].isdigit() or name.startswith('_')
or name in seen):
names[i] = '_%d' % i
seen.add(name)
field_names = tuple(names)
for name in (typename,) + field_names:
if not all(c.isalnum() or c == '_' for c in name):
raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: {0!r:s}'.format(name))
if iskeyword(name):
raise ValueError('Type names and field names cannot be a keyword: {0!r:s}'.format(name))
if name[0].isdigit():
raise ValueError('Type names and field names cannot start with a number: {0!r:s}'.format(name))
seen_names = set()
for name in field_names:
if name.startswith('_') and not rename:
raise ValueError('Field names cannot start with an underscore: {0!r:s}'.format(name))
if name in seen_names:
raise ValueError('Encountered duplicate field name: {0!r:s}'.format(name))
seen_names.add(name)
result = NamedTuple.__new__(NamedTuple, typename, (tuple, object), {'_fields': tuple(field_names)})
# For pickling to work, the __module__ variable 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:
result.__module__ = sys._getframe(1).f_globals.get('__name__', '__main__')
except (AttributeError, ValueError):
pass
return result
if __name__ == '__main__':
class Test(object, metaclass=NamedTuple):
_fields = ('alpha', 'beta', 'charlie')
t = Test(1, 2, charlie=3)
print('doc:', t.__doc__)
print('repr:', repr(t))
print('asdict:', t._asdict())
print('replace:', t._replace(alpha=4))
print('make:', Test._make([1, 2, 3]))
Zoo = namedtuple('Zoo', 'lions tigers bears', rename=True)
z = Zoo(8, 9, 10)
print(z)
print('lions:', z.lions)
print('tigers:', z.tigers)
print('bears:', z.bears)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment