From c37e5e04eb3cebd25ad9ceb3ee4ff09c658767a9 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Thu, 1 Mar 2007 06:16:43 +0000 Subject: Add collections.NamedTuple --- Doc/lib/libcollections.tex | 53 ++++++++++++++++++++++++++++++++++++-- Lib/collections.py | 61 +++++++++++++++++++++++++++++++++++++++++++- Lib/test/test_collections.py | 57 +++++++++++++++++++++++++++++++++++++++++ Misc/NEWS | 2 ++ 4 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 Lib/test/test_collections.py diff --git a/Doc/lib/libcollections.tex b/Doc/lib/libcollections.tex index a763e31..bdc14b5 100644 --- a/Doc/lib/libcollections.tex +++ b/Doc/lib/libcollections.tex @@ -9,14 +9,16 @@ This module implements high-performance container datatypes. Currently, -there are two datatypes, deque and defaultdict. +there are two datatypes, deque and defaultdict, and one datatype factory +function, \function{NamedTuple}. Future additions may include balanced trees and ordered dictionaries. \versionchanged[Added defaultdict]{2.5} +\versionchanged[Added NamedTuple]{2.6} \subsection{\class{deque} objects \label{deque-objects}} \begin{funcdesc}{deque}{\optional{iterable}} - Returns a new deque objected initialized left-to-right (using + Returns a new deque object initialized left-to-right (using \method{append()}) with data from \var{iterable}. If \var{iterable} is not specified, the new deque is empty. @@ -339,3 +341,50 @@ Setting the \member{default_factory} to \class{set} makes the >>> d.items() [('blue', set([2, 4])), ('red', set([1, 3]))] \end{verbatim} + + + +\subsection{\function{NamedTuple} datatype factory function \label{named-tuple-factory}} + +\begin{funcdesc}{NamedTuple}{typename, fieldnames} + Returns a new tuple subclass named \var{typename}. The new subclass is used + to create tuple-like objects that have fields accessable by attribute + lookup as well as being indexable and iterable. Instances of the subclass + also have a helpful docstring (with typename and fieldnames) and a helpful + \method{__repr__()} method which lists the tuple contents in a \code{name=value} + format. + \versionadded{2.6} + + The \var{fieldnames} are specified in a single string and are separated by spaces. + Any valid Python identifier may be used for a field name. + + Example: + \begin{verbatim} + >>> Point = NamedTuple('Point', 'x y') + >>> Point.__doc__ # docstring for the new datatype + 'Point(x, y)' + >>> p = Point(11, y=22) # instantiate with positional or keyword arguments + >>> p[0] + p[1] # works just like the tuple (11, 22) + 33 + >>> x, y = p # unpacks just like a tuple + >>> x, y + (11, 22) + >>> p.x + p.y # fields also accessable by name + 33 + >>> p # readable __repr__ with name=value style + Point(x=11, y=22) + \end{verbatim} + + The use cases are the same as those for tuples. The named factories + assign meaning to each tuple position and allow for more readable, + self-documenting code. Can also be used to assign field names to tuples + returned by the \module{csv} or \module{sqlite3} modules. For example: + + \begin{verbatim} + import csv + EmployeeRecord = NamedTuple('EmployeeRecord', 'name age title deparment paygrade') + for tup in csv.reader(open("employees.csv", "rb")): + print EmployeeRecord(*tup) + \end{verbatim} + +\end{funcdesc} diff --git a/Lib/collections.py b/Lib/collections.py index 0d9254c..6e816dc 100644 --- a/Lib/collections.py +++ b/Lib/collections.py @@ -1,3 +1,62 @@ -__all__ = ['deque', 'defaultdict'] +__all__ = ['deque', 'defaultdict', 'NamedTuple'] from _collections import deque, defaultdict +from operator import itemgetter as _itemgetter +import sys as _sys + +def NamedTuple(typename, s): + """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] # works just like the tuple (11, 22) + 33 + >>> x, y = p # unpacks just like a tuple + >>> x, y + (11, 22) + >>> p.x + p.y # fields also accessable by name + 33 + >>> p # readable __repr__ with name=value style + Point(x=11, y=22) + + """ + + field_names = s.split() + nargs = len(field_names) + + def __new__(cls, *args, **kwds): + if kwds: + try: + args += tuple(kwds[name] for name in field_names[len(args):]) + except KeyError, name: + raise TypeError('%s missing required argument: %s' % (typename, name)) + if len(args) != nargs: + raise TypeError('%s takes exactly %d arguments (%d given)' % (typename, nargs, len(args))) + return tuple.__new__(cls, args) + + repr_template = '%s(%s)' % (typename, ', '.join('%s=%%r' % name for name in field_names)) + + m = dict(vars(tuple)) # pre-lookup superclass methods (for faster lookup) + m.update(__doc__= '%s(%s)' % (typename, ', '.join(field_names)), + __slots__ = (), # no per-instance dict (so instances are same size as tuples) + __new__ = __new__, + __repr__ = lambda self, _format=repr_template.__mod__: _format(self), + __module__ = _sys._getframe(1).f_globals['__name__'], + ) + m.update((name, property(_itemgetter(index))) for index, name in enumerate(field_names)) + + return type(typename, (tuple,), m) + + +if __name__ == '__main__': + # verify that instances are pickable + from cPickle import loads, dumps + Point = NamedTuple('Point', 'x y') + p = Point(x=10, y=20) + assert p == loads(dumps(p)) + + import doctest + TestResults = NamedTuple('TestResults', 'failed attempted') + print TestResults(*doctest.testmod()) diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py new file mode 100644 index 0000000..a139129 --- /dev/null +++ b/Lib/test/test_collections.py @@ -0,0 +1,57 @@ +import unittest +from test import test_support +from collections import NamedTuple + +class TestNamedTuple(unittest.TestCase): + + def test_factory(self): + Point = NamedTuple('Point', 'x y') + self.assertEqual(Point.__name__, 'Point') + self.assertEqual(Point.__doc__, 'Point(x, y)') + self.assertEqual(Point.__slots__, ()) + self.assertEqual(Point.__module__, __name__) + self.assertEqual(Point.__getitem__, tuple.__getitem__) + self.assert_('__getitem__' in Point.__dict__) # superclass methods localized + + def test_instance(self): + Point = NamedTuple('Point', 'x y') + p = Point(11, 22) + self.assertEqual(p, Point(x=11, y=22)) + self.assertEqual(p, Point(11, y=22)) + self.assertEqual(p, Point(y=22, x=11)) + self.assertEqual(p, Point(*(11, 22))) + self.assertEqual(p, Point(**dict(x=11, y=22))) + self.assertRaises(TypeError, Point, 1) # too few args + self.assertRaises(TypeError, Point, 1, 2, 3) # too many args + self.assertRaises(TypeError, eval, 'Point(XXX=1, y=2)', locals()) # wrong keyword argument + self.assertRaises(TypeError, eval, 'Point(x=1)', locals()) # missing keyword argument + self.assertEqual(repr(p), 'Point(x=11, y=22)') + self.assert_('__dict__' not in dir(p)) # verify instance has no dict + self.assert_('__weakref__' not in dir(p)) + + def test_tupleness(self): + Point = NamedTuple('Point', 'x y') + p = Point(11, 22) + + self.assert_(isinstance(p, tuple)) + self.assertEqual(p, (11, 22)) # matches a real tuple + self.assertEqual(tuple(p), (11, 22)) # coercable to a real tuple + self.assertEqual(list(p), [11, 22]) # coercable to a list + self.assertEqual(max(p), 22) # iterable + self.assertEqual(max(*p), 22) # star-able + x, y = p + self.assertEqual(p, (x, y)) # unpacks like a tuple + self.assertEqual((p[0], p[1]), (11, 22)) # indexable like a tuple + self.assertRaises(IndexError, p.__getitem__, 3) + + self.assertEqual(p.x, x) + self.assertEqual(p.y, y) + self.assertRaises(AttributeError, eval, 'p.z', locals()) + + +def test_main(verbose=None): + test_classes = [TestNamedTuple] + test_support.run_unittest(*test_classes) + +if __name__ == "__main__": + test_main(verbose=True) diff --git a/Misc/NEWS b/Misc/NEWS index 4ef206a..cb828f0 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -138,6 +138,8 @@ Library - Added heapq.merge() for merging sorted input streams. +- Added collections.NamedTuple() for assigning field names to tuples. + - Added itertools.izip_longest(). - Have the encoding package's search function dynamically import using absolute -- cgit v0.12