summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Doc/library/collections.rst25
-rw-r--r--Lib/collections/__init__.py20
-rw-r--r--Lib/test/test_collections.py51
-rw-r--r--Misc/NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst1
4 files changed, 93 insertions, 4 deletions
diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst
index 4b0d8c0..18aaba65 100644
--- a/Doc/library/collections.rst
+++ b/Doc/library/collections.rst
@@ -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:
diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py
index 50cf814..7088b88 100644
--- a/Lib/collections/__init__.py
+++ b/Lib/collections/__init__.py
@@ -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,
diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py
index 6c466f4..cb66235 100644
--- a/Lib/test/test_collections.py
+++ b/Lib/test/test_collections.py
@@ -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):
diff --git a/Misc/NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst b/Misc/NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst
new file mode 100644
index 0000000..6e4aad8
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst
@@ -0,0 +1 @@
+collections.namedtuple() now supports default values.