bpo-32320: Add default value support to collections.namedtuple() (#4859)
This commit is contained in:
parent
d55209d5b1
commit
3948207c61
|
@ -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:
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
collections.namedtuple() now supports default values.
|
Loading…
Reference in New Issue