Minor fix-ups to named tuples:

* Make the _replace() method respect subclassing.

* Using property() to make _fields read-only wasn't a good idea.
  It caused len(Point._fields) to fail.

* Add note to _cast() about length checking and alternative with the star-operator.
This commit is contained in:
Raymond Hettinger 2008-01-04 03:22:53 +00:00
parent 123d5c9396
commit e0734e7dc0
3 changed files with 16 additions and 22 deletions

View File

@ -388,6 +388,8 @@ Example::
__slots__ = () __slots__ = ()
_fields = ('x', 'y')
def __new__(cls, x, y): def __new__(cls, x, y):
return tuple.__new__(cls, (x, y)) return tuple.__new__(cls, (x, y))
@ -402,11 +404,7 @@ Example::
def _replace(self, **kwds): def _replace(self, **kwds):
'Return a new Point object replacing specified fields with new values' 'Return a new Point object replacing specified fields with new values'
return Point._cast(map(kwds.get, ('x', 'y'), self)) return self.__class__._cast(map(kwds.get, ('x', 'y'), self))
@property
def _fields(self):
return ('x', 'y')
x = property(itemgetter(0)) x = property(itemgetter(0))
y = property(itemgetter(1)) y = property(itemgetter(1))
@ -439,17 +437,22 @@ by the :mod:`csv` or :mod:`sqlite3` modules::
print emp.name, emp.title print emp.name, emp.title
In addition to the methods inherited from tuples, named tuples support In addition to the methods inherited from tuples, named tuples support
three additonal methods and a read-only attribute. three additonal methods and one attribute.
.. method:: namedtuple._cast(iterable) .. method:: namedtuple._cast(iterable)
Class method returning a new instance taking the positional arguments from the *iterable*. Class method returning a new instance taking the positional arguments from the
Useful for casting existing sequences and iterables to named tuples: *iterable*. Useful for casting existing sequences and iterables to named tuples.
This fast constructor does not check the length of the inputs. To achieve the
same effect with length checking, use the star-operator instead.
:: ::
>>> t = [11, 22] >>> t = [11, 22]
>>> Point._cast(t) >>> Point._cast(t) # fast conversion
Point(x=11, y=22)
>>> Point(*t) # slow conversion with length checking
Point(x=11, y=22) Point(x=11, y=22)
.. method:: somenamedtuple._asdict() .. method:: somenamedtuple._asdict()
@ -476,7 +479,7 @@ three additonal methods and a read-only attribute.
.. attribute:: somenamedtuple._fields .. attribute:: somenamedtuple._fields
Return a tuple of strings listing the field names. This is useful for introspection Tuple of strings listing the field names. This is useful for introspection
and for creating new named tuple types from existing named tuples. and for creating new named tuple types from existing named tuples.
:: ::

View File

@ -60,6 +60,7 @@ def namedtuple(typename, field_names, verbose=False):
template = '''class %(typename)s(tuple): template = '''class %(typename)s(tuple):
'%(typename)s(%(argtxt)s)' \n '%(typename)s(%(argtxt)s)' \n
__slots__ = () \n __slots__ = () \n
_fields = %(field_names)r \n
def __new__(cls, %(argtxt)s): def __new__(cls, %(argtxt)s):
return tuple.__new__(cls, (%(argtxt)s)) \n return tuple.__new__(cls, (%(argtxt)s)) \n
_cast = classmethod(tuple.__new__) \n _cast = classmethod(tuple.__new__) \n
@ -70,10 +71,7 @@ def namedtuple(typename, field_names, verbose=False):
return {%(dicttxt)s} \n return {%(dicttxt)s} \n
def _replace(self, **kwds): def _replace(self, **kwds):
'Return a new %(typename)s object replacing specified fields with new values' 'Return a new %(typename)s object replacing specified fields with new values'
return %(typename)s._cast(map(kwds.get, %(field_names)r, self)) \n return self.__class__._cast(map(kwds.get, %(field_names)r, self)) \n\n''' % locals()
@property
def _fields(self):
return %(field_names)r \n\n''' % locals()
for i, name in enumerate(field_names): for i, name in enumerate(field_names):
template += ' %s = property(itemgetter(%d))\n' % (name, i) template += ' %s = property(itemgetter(%d))\n' % (name, i)
if verbose: if verbose:

View File

@ -17,6 +17,7 @@ class TestNamedTuple(unittest.TestCase):
self.assertEqual(Point.__slots__, ()) self.assertEqual(Point.__slots__, ())
self.assertEqual(Point.__module__, __name__) self.assertEqual(Point.__module__, __name__)
self.assertEqual(Point.__getitem__, tuple.__getitem__) self.assertEqual(Point.__getitem__, tuple.__getitem__)
self.assertEqual(Point._fields, ('x', 'y'))
self.assertRaises(ValueError, namedtuple, 'abc%', 'efg ghi') # type has non-alpha char self.assertRaises(ValueError, namedtuple, 'abc%', 'efg ghi') # type has non-alpha char
self.assertRaises(ValueError, namedtuple, 'class', 'efg ghi') # type has keyword self.assertRaises(ValueError, namedtuple, 'class', 'efg ghi') # type has keyword
@ -51,14 +52,6 @@ class TestNamedTuple(unittest.TestCase):
self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method self.assertEqual(p._replace(x=1), (1, 22)) # test _replace method
self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method self.assertEqual(p._asdict(), dict(x=11, y=22)) # test _asdict method
# Verify that _fields is read-only
try:
p._fields = ('F1' ,'F2')
except AttributeError:
pass
else:
self.fail('The _fields attribute needs to be read-only')
# verify that field string can have commas # verify that field string can have commas
Point = namedtuple('Point', 'x, y') Point = namedtuple('Point', 'x, y')
p = Point(x=11, y=22) p = Point(x=11, y=22)