From a5da93cd6be38e20c1eb2f950e01e5b7f6787efe Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 13 Dec 2017 23:15:58 -0800 Subject: [PATCH 1/4] Add defaults values. --- Lib/collections/__init__.py | 12 +++++++++--- Lib/test/test_collections.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 50cf8141731183..fc6cc3c2817e58 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, module=None, defaults=None): """Returns a new subclass of tuple with named fields. >>> Point = namedtuple('Point', ['x', 'y']) @@ -332,7 +332,7 @@ 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): @@ -359,6 +359,10 @@ def namedtuple(typename, field_names, *, rename=False, module=None): if name in seen: raise ValueError(f'Encountered duplicate field name: {name!r}') seen.add(name) + if defaults is not None: + defaults = tuple(defaults) + if len(defaults) > len(field_names): + raise TypeError('Got more default values than field names') # Variables used in the methods and docstrings field_names = tuple(map(_sys.intern, field_names)) @@ -372,10 +376,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): diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 7e106affbe032a..882f0cdfca7373 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -216,6 +216,37 @@ 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(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(1, 2), (1, 2)) + self.assertEqual(Point(1), (1, 20)) + + 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) + + Point = namedtuple('Point', 'x y', defaults=None) # default is None + 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.__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): From b18377c6837be2a936eca70b86d616b8b332100c Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Thu, 14 Dec 2017 00:37:28 -0800 Subject: [PATCH 2/4] Add _fields_defaults attribute and documentation. --- Doc/library/collections.rst | 23 ++++++++++++++++++++++- Lib/collections/__init__.py | 10 +++++++++- Lib/test/test_collections.py | 4 ++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 4b0d8c048ae7d9..725f03eb33c42a 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,16 @@ 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} + 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 fc6cc3c2817e58..7088b88e04a788 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, defaults=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']) @@ -333,6 +333,7 @@ def namedtuple(typename, field_names, *, rename=False, module=None, defaults=Non field_names = field_names.replace(',', ' ').split() field_names = list(map(str, field_names)) 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, defaults=Non 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, defaults=Non 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: @@ -359,10 +362,14 @@ def namedtuple(typename, field_names, *, rename=False, module=None, defaults=Non if name in seen: 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)) @@ -426,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 882f0cdfca7373..aa784a21e94e46 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -218,11 +218,13 @@ def test_factory(self): 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)) @@ -236,12 +238,14 @@ def test_defaults(self): Point = namedtuple('Point', 'x y', defaults=10) 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)) From 57f6f143e23b9d8754e6cb2c0c276d945eb30a2a Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Thu, 14 Dec 2017 01:36:53 -0800 Subject: [PATCH 3/4] Add news blurb --- .../NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst 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 00000000000000..6e4aad8f795adf --- /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. From f636dc5c6dd686d312acf48e26caa03f966f88fd Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 10 Jan 2018 21:32:33 -0800 Subject: [PATCH 4/4] Add more tests and another example --- Doc/library/collections.rst | 2 ++ Lib/test/test_collections.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 725f03eb33c42a..18aaba65b252c3 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -931,6 +931,8 @@ field names, the method and attribute names start with an underscore. >>> 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/test/test_collections.py b/Lib/test/test_collections.py index aa784a21e94e46..66f4ecfd0f0127 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -228,6 +228,12 @@ def test_defaults(self): 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 @@ -236,6 +242,8 @@ def test_defaults(self): 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, {}) @@ -251,6 +259,14 @@ def test_defaults(self): 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):