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

This commit is contained in:
Raymond Hettinger 2018-01-10 21:45:19 -08:00 committed by GitHub
parent d55209d5b1
commit 3948207c61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 93 additions and 4 deletions

View File

@ -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
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
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.
converted to ``['abc', '_1', 'ghi', '_3']``, eliminating the keyword
``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
set to that value.
@ -824,6 +831,10 @@ they add the ability to access fields by name instead of position index.
.. versionchanged:: 3.7
Remove the *verbose* parameter and the :attr:`_source` attribute.
.. versionchanged:: 3.7
Added the *defaults* parameter and the :attr:`_field_defaults`
attribute.
.. doctest::
:options: +NORMALIZE_WHITESPACE
@ -911,6 +922,18 @@ field names, the method and attribute names start with an underscore.
>>> Pixel(11, 22, 128, 255, 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`
function:

View File

@ -303,7 +303,7 @@ except ImportError:
_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.
>>> Point = namedtuple('Point', ['x', 'y'])
@ -332,7 +332,8 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
if isinstance(field_names, str):
field_names = field_names.replace(',', ' ').split()
field_names = list(map(str, field_names))
typename = str(typename)
typename = _sys.intern(str(typename))
if rename:
seen = set()
for index, name in enumerate(field_names):
@ -342,6 +343,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
or name in seen):
field_names[index] = f'_{index}'
seen.add(name)
for name in [typename] + field_names:
if type(name) is not str:
raise TypeError('Type names and field names must be strings')
@ -351,6 +353,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
if _iskeyword(name):
raise ValueError('Type names and field names cannot be a '
f'keyword: {name!r}')
seen = set()
for name in field_names:
if name.startswith('_') and not rename:
@ -360,6 +363,14 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
raise ValueError(f'Encountered duplicate field name: {name!r}')
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
field_names = tuple(map(_sys.intern, field_names))
num_fields = len(field_names)
@ -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}))'
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)
__new__ = namespace['__new__']
__new__.__doc__ = f'Create new instance of {typename}({arg_list})'
if defaults is not None:
__new__.__defaults__ = defaults
@classmethod
def _make(cls, iterable):
@ -420,6 +433,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
'__doc__': f'{typename}({arg_list})',
'__slots__': (),
'_fields': field_names,
'_fields_defaults': field_defaults,
'__new__': __new__,
'_make': _make,
'_replace': _replace,

View File

@ -216,6 +216,57 @@ class TestNamedTuple(unittest.TestCase):
self.assertRaises(TypeError, Point._make, [11]) # catch too few 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,
"Docstrings are omitted with -O2 and above")
def test_factory_doc_attr(self):

View File

@ -0,0 +1 @@
collections.namedtuple() now supports default values.