diff options
-rw-r--r-- | Doc/library/collections.rst | 25 | ||||
-rw-r--r-- | Lib/collections/__init__.py | 20 | ||||
-rw-r--r-- | Lib/test/test_collections.py | 51 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2017-12-14-01-36-25.bpo-32320.jwOZlr.rst | 1 |
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. |