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

Raymond Hettinger webhook-mailer at python.org
Thu Jan 11 00:45:22 EST 2018


https://github.com/python/cpython/commit/3948207c610e931831828d33aaef258185df31db
commit: 3948207c610e931831828d33aaef258185df31db
branch: master
author: Raymond Hettinger <rhettinger at users.noreply.github.com>
committer: GitHub <noreply at github.com>
date: 2018-01-10T21:45:19-08:00
summary:

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

files:
A Misc/NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst
M Doc/library/collections.rst
M Lib/collections/__init__.py
M Lib/test/test_collections.py

diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst
index 4b0d8c048ae..18aaba65b25 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 50cf8141731..7088b88e04a 100644
--- a/Lib/collections/__init__.py
+++ b/Lib/collections/__init__.py
@@ -303,7 +303,7 @@ def __eq__(self, other):
 
 _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 __getnewargs__(self):
         '__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 6c466f41e68..cb662355bb7 100644
--- a/Lib/test/test_collections.py
+++ b/Lib/test/test_collections.py
@@ -216,6 +216,57 @@ def test_factory(self):
         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 00000000000..6e4aad8f795
--- /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.



More information about the Python-checkins mailing list