From 85dfcf35304935521085253ce5573feab21177d5 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Tue, 18 Dec 2007 23:51:15 +0000 Subject: [PATCH] Users demand iterable input for named tuples. The author capitulates. --- Doc/library/collections.rst | 39 ++++++++++++++++++++---------------- Lib/collections.py | 3 ++- Lib/test/test_collections.py | 4 ++++ 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 5fcb293c610..9e8f3e36699 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -391,6 +391,8 @@ Example:: def __new__(cls, x, y): return tuple.__new__(cls, (x, y)) + _cast = classmethod(tuple.__new__) + def __repr__(self): return 'Point(x=%r, y=%r)' % self @@ -400,7 +402,7 @@ Example:: def _replace(self, **kwds): 'Return a new Point object replacing specified fields with new values' - return Point(*map(kwds.get, ('x', 'y'), self)) + return Point._cast(map(kwds.get, ('x', 'y'), self)) @property def _fields(self): @@ -425,34 +427,31 @@ by the :mod:`csv` or :mod:`sqlite3` modules:: EmployeeRecord = namedtuple('EmployeeRecord', 'name, age, title, department, paygrade') - from itertools import starmap import csv - for emp in starmap(EmployeeRecord, csv.reader(open("employees.csv", "rb"))): + for emp in map(EmployeeRecord._cast, csv.reader(open("employees.csv", "rb"))): print emp.name, emp.title import sqlite3 conn = sqlite3.connect('/companydata') cursor = conn.cursor() cursor.execute('SELECT name, age, title, department, paygrade FROM employees') - for emp in starmap(EmployeeRecord, cursor.fetchall()): + for emp in map(EmployeeRecord._cast, cursor.fetchall()): print emp.name, emp.title -When casting a single record to a named tuple, use the star-operator [#]_ to unpack -the values:: +In addition to the methods inherited from tuples, named tuples support +three additonal methods and a read-only attribute. + +.. method:: namedtuple._cast(iterable) + + Class method returning a new instance taking the positional arguments from the *iterable*. + Useful for casting existing sequences and iterables to named tuples: + +:: >>> t = [11, 22] - >>> Point(*t) # the star-operator unpacks any iterable object + >>> Point._cast(t) Point(x=11, y=22) -When casting a dictionary to a named tuple, use the double-star-operator:: - - >>> d = {'x': 11, 'y': 22} - >>> Point(**d) - Point(x=11, y=22) - -In addition to the methods inherited from tuples, named tuples support -two additonal methods and a read-only attribute. - .. method:: somenamedtuple._asdict() Return a new dict which maps field names to their corresponding values: @@ -498,6 +497,12 @@ function: >>> getattr(p, 'x') 11 +When casting a dictionary to a named tuple, use the double-star-operator [#]_:: + + >>> d = {'x': 11, 'y': 22} + >>> Point(**d) + Point(x=11, y=22) + Since a named tuple is a regular Python class, it is easy to add or change functionality. For example, the display format can be changed by overriding the :meth:`__repr__` method: @@ -520,5 +525,5 @@ and customizing it with :meth:`_replace`: .. rubric:: Footnotes -.. [#] For information on the star-operator see +.. [#] For information on the double-star-operator see :ref:`tut-unpacking-arguments` and :ref:`calls`. diff --git a/Lib/collections.py b/Lib/collections.py index 3d98f7c5b98..c6d0d0f1f1e 100644 --- a/Lib/collections.py +++ b/Lib/collections.py @@ -62,6 +62,7 @@ def namedtuple(typename, field_names, verbose=False): __slots__ = () \n def __new__(cls, %(argtxt)s): return tuple.__new__(cls, (%(argtxt)s)) \n + _cast = classmethod(tuple.__new__) \n def __repr__(self): return '%(typename)s(%(reprtxt)s)' %% self \n def _asdict(t): @@ -69,7 +70,7 @@ def namedtuple(typename, field_names, verbose=False): return {%(dicttxt)s} \n def _replace(self, **kwds): 'Return a new %(typename)s object replacing specified fields with new values' - return %(typename)s(*map(kwds.get, %(field_names)r, self)) \n + return %(typename)s._cast(map(kwds.get, %(field_names)r, self)) \n @property def _fields(self): return %(field_names)r \n\n''' % locals() diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 73ae62ade5d..edffbbe9323 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -46,6 +46,7 @@ class TestNamedTuple(unittest.TestCase): 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)) + self.assertEqual(p, Point._cast([11, 22])) # test _cast classmethod self.assertEqual(p._fields, ('x', 'y')) # test _fields attribute self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method @@ -90,12 +91,14 @@ class TestNamedTuple(unittest.TestCase): def test_odd_sizes(self): Zero = namedtuple('Zero', '') self.assertEqual(Zero(), ()) + self.assertEqual(Zero._cast([]), ()) self.assertEqual(repr(Zero()), 'Zero()') self.assertEqual(Zero()._asdict(), {}) self.assertEqual(Zero()._fields, ()) Dot = namedtuple('Dot', 'd') self.assertEqual(Dot(1), (1,)) + self.assertEqual(Dot._cast([1]), (1,)) self.assertEqual(Dot(1).d, 1) self.assertEqual(repr(Dot(1)), 'Dot(d=1)') self.assertEqual(Dot(1)._asdict(), {'d':1}) @@ -108,6 +111,7 @@ class TestNamedTuple(unittest.TestCase): Big = namedtuple('Big', names) b = Big(*range(n)) self.assertEqual(b, tuple(range(n))) + self.assertEqual(Big._cast(range(n)), tuple(range(n))) for pos, name in enumerate(names): self.assertEqual(getattr(b, name), pos) repr(b) # make sure repr() doesn't blow-up