Commit 3948207c authored by Raymond Hettinger's avatar Raymond Hettinger Committed by GitHub

bpo-32320: Add default value support to collections.namedtuple() (#4859)

parent d55209d5
...@@ -782,7 +782,7 @@ Named tuples assign meaning to each position in a tuple and allow for more reada ...@@ -782,7 +782,7 @@ Named tuples assign meaning to each position in a tuple and allow for more reada
self-documenting code. They can be used wherever regular tuples are used, and self-documenting code. They can be used wherever regular tuples are used, and
they add the ability to access fields by name instead of position index. they add the ability to access fields by name instead of position index.
.. function:: namedtuple(typename, field_names, *, rename=False, module=None) .. function:: namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)
Returns a new tuple subclass named *typename*. The new subclass is used to Returns a new tuple subclass named *typename*. The new subclass is used to
create tuple-like objects that have fields accessible by attribute lookup as create tuple-like objects that have fields accessible by attribute lookup as
...@@ -805,6 +805,13 @@ they add the ability to access fields by name instead of position index. ...@@ -805,6 +805,13 @@ they add the ability to access fields by name instead of position index.
converted to ``['abc', '_1', 'ghi', '_3']``, eliminating the keyword converted to ``['abc', '_1', 'ghi', '_3']``, eliminating the keyword
``def`` and the duplicate fieldname ``abc``. ``def`` and the duplicate fieldname ``abc``.
*defaults* can be ``None`` or an :term:`iterable` of default values.
Since fields with a default value must come after any fields without a
default, the *defaults* are applied to the rightmost parameters. For
example, if the fieldnames are ``['x', 'y', 'z']`` and the defaults are
``(1, 2)``, then ``x`` will be a required argument, ``y`` will default to
``1``, and ``z`` will default to ``2``.
If *module* is defined, the ``__module__`` attribute of the named tuple is If *module* is defined, the ``__module__`` attribute of the named tuple is
set to that value. set to that value.
...@@ -824,6 +831,10 @@ they add the ability to access fields by name instead of position index. ...@@ -824,6 +831,10 @@ they add the ability to access fields by name instead of position index.
.. versionchanged:: 3.7 .. versionchanged:: 3.7
Remove the *verbose* parameter and the :attr:`_source` attribute. Remove the *verbose* parameter and the :attr:`_source` attribute.
.. versionchanged:: 3.7
Added the *defaults* parameter and the :attr:`_field_defaults`
attribute.
.. doctest:: .. doctest::
:options: +NORMALIZE_WHITESPACE :options: +NORMALIZE_WHITESPACE
...@@ -911,6 +922,18 @@ field names, the method and attribute names start with an underscore. ...@@ -911,6 +922,18 @@ field names, the method and attribute names start with an underscore.
>>> Pixel(11, 22, 128, 255, 0) >>> Pixel(11, 22, 128, 255, 0)
Pixel(x=11, y=22, red=128, green=255, blue=0) Pixel(x=11, y=22, red=128, green=255, blue=0)
.. attribute:: somenamedtuple._fields_defaults
Dictionary mapping field names to default values.
.. doctest::
>>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
>>> Account._fields_defaults
{'balance': 0}
>>> Account('premium')
Account(type='premium', balance=0)
To retrieve a field whose name is stored in a string, use the :func:`getattr` To retrieve a field whose name is stored in a string, use the :func:`getattr`
function: function:
......
...@@ -303,7 +303,7 @@ except ImportError: ...@@ -303,7 +303,7 @@ except ImportError:
_nt_itemgetters = {} _nt_itemgetters = {}
def namedtuple(typename, field_names, *, rename=False, module=None): def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
"""Returns a new subclass of tuple with named fields. """Returns a new subclass of tuple with named fields.
>>> Point = namedtuple('Point', ['x', 'y']) >>> Point = namedtuple('Point', ['x', 'y'])
...@@ -332,7 +332,8 @@ def namedtuple(typename, field_names, *, rename=False, module=None): ...@@ -332,7 +332,8 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
if isinstance(field_names, str): if isinstance(field_names, str):
field_names = field_names.replace(',', ' ').split() field_names = field_names.replace(',', ' ').split()
field_names = list(map(str, field_names)) field_names = list(map(str, field_names))
typename = str(typename) typename = _sys.intern(str(typename))
if rename: if rename:
seen = set() seen = set()
for index, name in enumerate(field_names): for index, name in enumerate(field_names):
...@@ -342,6 +343,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None): ...@@ -342,6 +343,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
or name in seen): or name in seen):
field_names[index] = f'_{index}' field_names[index] = f'_{index}'
seen.add(name) seen.add(name)
for name in [typename] + field_names: for name in [typename] + field_names:
if type(name) is not str: if type(name) is not str:
raise TypeError('Type names and field names must be strings') raise TypeError('Type names and field names must be strings')
...@@ -351,6 +353,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None): ...@@ -351,6 +353,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
if _iskeyword(name): if _iskeyword(name):
raise ValueError('Type names and field names cannot be a ' raise ValueError('Type names and field names cannot be a '
f'keyword: {name!r}') f'keyword: {name!r}')
seen = set() seen = set()
for name in field_names: for name in field_names:
if name.startswith('_') and not rename: if name.startswith('_') and not rename:
...@@ -360,6 +363,14 @@ def namedtuple(typename, field_names, *, rename=False, module=None): ...@@ -360,6 +363,14 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
raise ValueError(f'Encountered duplicate field name: {name!r}') raise ValueError(f'Encountered duplicate field name: {name!r}')
seen.add(name) seen.add(name)
field_defaults = {}
if defaults is not None:
defaults = tuple(defaults)
if len(defaults) > len(field_names):
raise TypeError('Got more default values than field names')
field_defaults = dict(reversed(list(zip(reversed(field_names),
reversed(defaults)))))
# Variables used in the methods and docstrings # Variables used in the methods and docstrings
field_names = tuple(map(_sys.intern, field_names)) field_names = tuple(map(_sys.intern, field_names))
num_fields = len(field_names) num_fields = len(field_names)
...@@ -372,10 +383,12 @@ def namedtuple(typename, field_names, *, rename=False, module=None): ...@@ -372,10 +383,12 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
s = f'def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list}))' s = f'def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list}))'
namespace = {'_tuple_new': tuple_new, '__name__': f'namedtuple_{typename}'} namespace = {'_tuple_new': tuple_new, '__name__': f'namedtuple_{typename}'}
# Note: exec() has the side-effect of interning the typename and field names # Note: exec() has the side-effect of interning the field names
exec(s, namespace) exec(s, namespace)
__new__ = namespace['__new__'] __new__ = namespace['__new__']
__new__.__doc__ = f'Create new instance of {typename}({arg_list})' __new__.__doc__ = f'Create new instance of {typename}({arg_list})'
if defaults is not None:
__new__.__defaults__ = defaults
@classmethod @classmethod
def _make(cls, iterable): def _make(cls, iterable):
...@@ -420,6 +433,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None): ...@@ -420,6 +433,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
'__doc__': f'{typename}({arg_list})', '__doc__': f'{typename}({arg_list})',
'__slots__': (), '__slots__': (),
'_fields': field_names, '_fields': field_names,
'_fields_defaults': field_defaults,
'__new__': __new__, '__new__': __new__,
'_make': _make, '_make': _make,
'_replace': _replace, '_replace': _replace,
......
...@@ -216,6 +216,57 @@ class TestNamedTuple(unittest.TestCase): ...@@ -216,6 +216,57 @@ class TestNamedTuple(unittest.TestCase):
self.assertRaises(TypeError, Point._make, [11]) # catch too few args self.assertRaises(TypeError, Point._make, [11]) # catch too few args
self.assertRaises(TypeError, Point._make, [11, 22, 33]) # catch too many args self.assertRaises(TypeError, Point._make, [11, 22, 33]) # catch too many args
def test_defaults(self):
Point = namedtuple('Point', 'x y', defaults=(10, 20)) # 2 defaults
self.assertEqual(Point._fields_defaults, {'x': 10, 'y': 20})
self.assertEqual(Point(1, 2), (1, 2))
self.assertEqual(Point(1), (1, 20))
self.assertEqual(Point(), (10, 20))
Point = namedtuple('Point', 'x y', defaults=(20,)) # 1 default
self.assertEqual(Point._fields_defaults, {'y': 20})
self.assertEqual(Point(1, 2), (1, 2))
self.assertEqual(Point(1), (1, 20))
Point = namedtuple('Point', 'x y', defaults=()) # 0 defaults
self.assertEqual(Point._fields_defaults, {})
self.assertEqual(Point(1, 2), (1, 2))
with self.assertRaises(TypeError):
Point(1)
with self.assertRaises(TypeError): # catch too few args
Point()
with self.assertRaises(TypeError): # catch too many args
Point(1, 2, 3)
with self.assertRaises(TypeError): # too many defaults
Point = namedtuple('Point', 'x y', defaults=(10, 20, 30))
with self.assertRaises(TypeError): # non-iterable defaults
Point = namedtuple('Point', 'x y', defaults=10)
with self.assertRaises(TypeError): # another non-iterable default
Point = namedtuple('Point', 'x y', defaults=False)
Point = namedtuple('Point', 'x y', defaults=None) # default is None
self.assertEqual(Point._fields_defaults, {})
self.assertIsNone(Point.__new__.__defaults__, None)
self.assertEqual(Point(10, 20), (10, 20))
with self.assertRaises(TypeError): # catch too few args
Point(10)
Point = namedtuple('Point', 'x y', defaults=[10, 20]) # allow non-tuple iterable
self.assertEqual(Point._fields_defaults, {'x': 10, 'y': 20})
self.assertEqual(Point.__new__.__defaults__, (10, 20))
self.assertEqual(Point(1, 2), (1, 2))
self.assertEqual(Point(1), (1, 20))
self.assertEqual(Point(), (10, 20))
Point = namedtuple('Point', 'x y', defaults=iter([10, 20])) # allow plain iterator
self.assertEqual(Point._fields_defaults, {'x': 10, 'y': 20})
self.assertEqual(Point.__new__.__defaults__, (10, 20))
self.assertEqual(Point(1, 2), (1, 2))
self.assertEqual(Point(1), (1, 20))
self.assertEqual(Point(), (10, 20))
@unittest.skipIf(sys.flags.optimize >= 2, @unittest.skipIf(sys.flags.optimize >= 2,
"Docstrings are omitted with -O2 and above") "Docstrings are omitted with -O2 and above")
def test_factory_doc_attr(self): def test_factory_doc_attr(self):
......
collections.namedtuple() now supports default values.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment