8000 bpo-32320: Add default value support to collections.namedtuple() (#4859) · python/cpython@3948207 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3948207

Browse files
authored
bpo-32320: Add default value support to collections.namedtuple() (#4859)
1 parent d55209d commit 3948207

File tree

4 files changed

+93
-4
lines changed

4 files changed

+93
-4
lines changed

Doc/library/collections.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ Named tuples assign meaning to each position in a tuple and allow for more reada
782782
self-documenting code. They can be used wherever regular tuples are used, and
783783
they add the ability to access fields by name instead of position index.
784784

785-
.. function:: namedtuple(typename, field_names, *, rename=False, module=None)
785+
.. function:: namedtuple(typename, field_names, *, rename=False, defaults=None, module=None)
786786

787787
Returns a new tuple subclass named *typename*. The new subclass is used to
788788
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.
805805
converted to ``['abc', '_1', 'ghi', '_3']``, eliminating the keyword
806806
``def`` and the duplicate fieldname ``abc``.
807807

808+
*defaults* can be ``None`` or an :term:`iterable` of default values.
809+
Since fields with a default value must come after any fields without a
810+
default, the *defaults* are applied to the rightmost parameters. For
811+
example, if the fieldnames are ``['x', 'y', 'z']`` and the defaults are
812+
``(1, 2)``, then ``x`` will be a required argument, ``y`` will default to
813+
``1``, and ``z`` will default to ``2``.
814+
808815
If *module* is defined, the ``__module__`` attribute of the named tuple is
809816
set to that value.
810817

@@ -824,6 +831,10 @@ they add the ability to access fields by name instead of position index.
824831
.. versionchanged:: 3.7
825832
Remove the *verbose* parameter and the :attr:`_source` attribute.
826833

834+
.. versionchanged:: 3.7
835+
Added the *defaults* parameter and the :attr:`_field_defaults`
836+
attribute.
837+
827838
.. doctest::
828839
:options: +NORMALIZE_WHITESPACE
829840

@@ -911,6 +922,18 @@ field names, the method and attribute names start with an underscore.
911922
>>> Pixel(11, 22, 128, 255, 0)
912923
Pixel(x=11, y=22, red=128, green=255, blue=0)
913924

925+
.. attribute:: somenamedtuple._fields_defaults
926+
927+
Dictionary mapping field names to default values.
928+
929+
.. doctest::
930+
931+
>>> Account = namedtuple('Account', ['type', 'balance'], defaults=[0])
932+
>>> Account._fields_defaults
933+
{'balance': 0}
934+
>>> Account('premium')
935+
Account(type='premium', balance=0)
936+
914937
To retrieve a field whose name is stored in a string, use the :func:`getattr`
915938
function:
916939

Lib/collections/__init__.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ def __eq__(self, other):
303303

304304
_nt_itemgetters = {}
305305

306-
def namedtuple(typename, field_names, *, rename=False, module=None):
306+
def namedtuple(typename, field_names, *, rename=False, defaults=None, module=None):
307307
"""Returns a new subclass of tuple with named fields.
308308
309309
>>> Point = namedtuple('Point', ['x', 'y'])
@@ -332,7 +332,8 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
332332
if isinstance(field_names, str):
333333
field_names = field_names.replace(',', ' ').split()
334334
field_names = list(map(str, field_names))
335-
typename = str(typename)
335+
typename = _sys.intern(str(typename))
336+
336337
if rename:
337338
seen = set()
338339
for index, name in enumerate(field_names):
@@ -342,6 +343,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
342343
or name in seen):
343344
field_names[index] = f'_{index}'
344345
seen.add(name)
346+
345347
for name in [typename] + field_names:
346348
if type(name) is not str:
347349
raise TypeError('Type names and field names must be strings')
@@ -351,6 +353,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
351353
if _iskeyword(name):
352354
raise ValueError('Type names and field names cannot be a '
353355
f'keyword: {name!r}')
356+
354357
seen = set()
355358
for name in field_names:
356359
if name.startswith('_') and not rename:
@@ -360,6 +363,14 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
360363
raise ValueError(f'Encountered duplicate field name: {name!r}')
361364
seen.add(name)
362365

366+
field_defaults = {}
367+
if defaults is not None:
368+
defaults = tuple(defaults)
369+
if len(defaults) > len(field_names):
370+
raise TypeError('Got more default values than field names')
371+
field_defaults = dict(reversed(list(zip(reversed(field_names),
372+
reversed(defaults)))))
373+
363374
# Variables used in the methods and docstrings
364375
field_names = tuple(map(_sys.intern, field_names))
365376
num_fields = len(field_names)
@@ -372,10 +383,12 @@ def namedtuple(typename, field_names, *, rename=False, module=None):
372383

373384
s = f'def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list}))'
374385
namespace = {'_tuple_new': tuple_new, '__name__': f'namedtuple_{typename}'}
375-
# Note: exec() has the side-effect of interning the typename and field names
386+
# Note: exec() has the side-effect of interning the field names
376387
exec(s, namespace)
377388
__new__ = namespace['__new__']
378389
__new__.__doc__ = f'Create new instance of {typename}({arg_list})'
390+
if defaults is not None:
391+
__new__.__defaults__ = defaults
379392

380393
@classmethod
381394
def _make(cls, iterable):
@@ -420,6 +433,7 @@ def __getnewargs__(self):
420433
'__doc__': f'{typename}({arg_list})',
421434
'__slots__': (),
422435
'_fields': field_names,
436+
'_fields_defaults': field_defaults,
423437
'__new__': __new__,
424438
'_make': _make,
425439
'_replace': _replace,

Lib/test/test_collections.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,57 @@ def test_factory(self):
216216
self.assertRaises(TypeError, Point._make, [11]) # catch too few args
217217
self.assertRaises(TypeError, Point._make, [11, 22, 33]) # catch too many args
218218

219+
def test_defaults(self):
220+
Point = namedtuple('Point', 'x y', defaults=(10, 20)) # 2 defaults
221+
self.assertEqual(Point._fields_defaults, {'x': 10, 'y': 20})
222+
self.assertEqual(Point(1, 2), (1, 2))
223+
self.assertEqual(Point(1), (1, 20))
224+
self.assertEqual(Point(), (10, 20))
225+
226+
Point = namedtuple('Point', 'x y', defaults=(20,)) # 1 default
227+
self.assertEqual(Point._fields_defaults, {'y': 20})
228+
self.assertEqual(Point(1, 2), (1, 2))
229+
self.assertEqual(Point(1), (1, 20))
230+
231+
Point = namedtuple('Point', 'x y', defaults=()) # 0 defaults
232+
self.assertEqual(Point._fields_defaults, {})
233+
self.assertEqual(Point(1, 2), (1, 2))
234+
with self.assertRaises(TypeError):
235+
Point(1)
236+
237+
with self.assertRaises(TypeError): # catch too few args
238+
Point()
239+
with self.assertRaises(TypeError): # catch too many args
240+
Point(1, 2, 3)
241+
with self.assertRaises(TypeError): # too many defaults
242+
Point = namedtuple('Point', 'x y', defaults=(10, 20, 30))
243+
with self.assertRaises(TypeError): # non-iterable defaults
244+
Point = namedtuple('Point', 'x y', defaults=10)
245+
with self.assertRaises(TypeError): # another non-iterable default
246+
Point = namedtuple('Point', 'x y', defaults=False)
247+
248+
Point = namedtuple('Point', 'x y', defaults=None) # default is None
249+
self.assertEqual(Point._fields_defaults, {})
250+
self.assertIsNone(Point.__new__.__defaults__, None)
251+
self.assertEqual(Point(10, 20), (10, 20))
252+
with self.assertRaises(TypeError): # catch too few args
253+
Point(10)
254+
255+
Point = namedtuple('Point', 'x y', defaults=[10, 20]) # allow non-tuple iterable
256+
self.assertEqual(Point._fields_defaults, {'x': 10, 'y': 20})
257+
self.assertEqual(Point.__new__.__defaults__, (10, 20))
258+
self.assertEqual(Point(1, 2), (1, 2))
259+
self.assertEqual(Point(1), (1, 20))
260+
self.assertEqual(Point(), (10, 20))
261+
262+
Point = namedtuple('Point', 'x y', defaults=iter([10, 20])) # allow plain iterator
263+
self.assertEqual(Point._fields_defaults, {'x': 10, 'y': 20})
264+
self.assertEqual(Point.__new__.__defaults__, (10, 20))
265+
self.assertEqual(Point(1, 2), (1, 2))
266+
self.assertEqual(Point(1), (1, 20))
267+
self.assertEqual(Point(), (10, 20))
268+
269+
219270
@unittest.skipIf(sys.flags.optimize >= 2,
220271
"Docstrings are omitted with -O2 and above")
221272
def test_factory_doc_attr(self):
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
collections.namedtuple() now supports default values.

0 commit comments

Comments
 (0)
0