From 5634af4ccfd06a2fabc2cc2cfcc9c014caf6f389 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sat, 15 Jul 2017 10:30:39 -0700 Subject: [PATCH 01/10] namedtuple: speed up creation by avoiding exec Creating a namedtuple is relatively slow because it uses exec(). This commit reduces the exec()'ed code, but still uses exec() for creating the __new__ method. I don't know of a way to avoid using exec() for __new__ beyond manipulating bytecode directly. However, avoiding exec() for creating the class itself still yields a significant speedup. In an unscientific benchmark I ran, creating 1000 namedtuple classes now takes about 0.14 s instead of 0.44 s. There is one backward compatibility break: namedtuples no longer have a _source attribute, because we no longer exec() their source. I kept the verbose=True argument around for compatibility, but it now does nothing. --- Lib/collections/__init__.py | 126 ++++++++++++++++------------------- Lib/test/test_collections.py | 15 ----- 2 files changed, 59 insertions(+), 82 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 8408255d27ea6a..4c55f963c41477 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -303,58 +303,14 @@ def __eq__(self, other): ### namedtuple ################################################################################ -_class_template = """\ -from builtins import property as _property, tuple as _tuple -from operator import itemgetter as _itemgetter -from collections import OrderedDict - -class {typename}(tuple): - '{typename}({arg_list})' - - __slots__ = () - - _fields = {field_names!r} - - def __new__(_cls, {arg_list}): - 'Create new instance of {typename}({arg_list})' - return _tuple.__new__(_cls, ({arg_list})) - - @classmethod - def _make(cls, iterable, new=tuple.__new__, len=len): - 'Make a new {typename} object from a sequence or iterable' - result = new(cls, iterable) - if len(result) != {num_fields:d}: - raise TypeError('Expected {num_fields:d} arguments, got %d' % len(result)) - return result - - def _replace(_self, **kwds): - 'Return a new {typename} object replacing specified fields with new values' - result = _self._make(map(kwds.pop, {field_names!r}, _self)) - if kwds: - raise ValueError('Got unexpected field names: %r' % list(kwds)) - return result - - def __repr__(self): - 'Return a nicely formatted representation string' - return self.__class__.__name__ + '({repr_fmt})' % self - - def _asdict(self): - 'Return a new OrderedDict which maps field names to their values.' - return OrderedDict(zip(self._fields, self)) - - def __getnewargs__(self): - 'Return self as a plain tuple. Used by copy and pickle.' - return tuple(self) - -{field_defs} -""" - _repr_template = '{name}=%r' - -_field_template = '''\ - {name} = _property(_itemgetter({index:d}), doc='Alias for field number {index:d}') +_new_template = ''' +def __new__(_cls, {arg_list}): + 'Create new instance of {typename}({arg_list})' + return _tuple.__new__(_cls, ({arg_list})) ''' + def namedtuple(typename, field_names, *, verbose=False, rename=False, module=None): """Returns a new subclass of tuple with named fields. @@ -412,26 +368,62 @@ def namedtuple(typename, field_names, *, verbose=False, rename=False, module=Non raise ValueError('Encountered duplicate field name: %r' % name) seen.add(name) - # Fill-in the class template - class_definition = _class_template.format( - typename = typename, - field_names = tuple(field_names), - num_fields = len(field_names), - arg_list = repr(tuple(field_names)).replace("'", "")[1:-1], - repr_fmt = ', '.join(_repr_template.format(name=name) - for name in field_names), - field_defs = '\n'.join(_field_template.format(index=index, name=name) - for index, name in enumerate(field_names)) + arg_list = repr(tuple(field_names)).replace("'", "")[1:-1] + num_fields = len(field_names) + repr_fmt = '(' + ', '.join(_repr_template.format(name=name) for name in field_names) + ')' + + namespace = {'_tuple': tuple} + exec(_new_template.format(typename=typename, arg_list=arg_list), namespace) + __new__ = namespace['__new__'] + + @classmethod + def _make(cls, iterable, new=tuple.__new__, len=len): + result = new(cls, iterable) + if len(result) != num_fields: + raise TypeError('Expected %d arguments, got %d' % (num_fields, len(result))) + return result + + _make.__func__.__doc__ = 'Make a new {typename} object from a sequence or iterable'.format(typename=typename) + + def _replace(_self, **kwds): + result = _self._make(map(kwds.pop, field_names, _self)) + if kwds: + raise ValueError('Got unexpected field names: %r' % list(kwds)) + return result + + _replace.__doc__ = ( + 'Return a new {typename} object replacing specified fields with new values'.format( + typename=typename) ) - # Execute the template string in a temporary namespace and support - # tracing utilities by setting a value for frame.f_globals['__name__'] - namespace = dict(__name__='namedtuple_%s' % typename) - exec(class_definition, namespace) - result = namespace[typename] - result._source = class_definition - if verbose: - print(result._source) + def __repr__(self): + 'Return a nicely formatted representation string' + return self.__class__.__name__ + repr_fmt % self + + def _asdict(self): + 'Return a new OrderedDict which maps field names to their values.' + return OrderedDict(zip(self._fields, self)) + + def __getnewargs__(self): + 'Return self as a plain tuple. Used by copy and pickle.' + return tuple(self) + + class_namespace = { + '__doc__': '{typename}({arg_list})'.format(typename=typename, arg_list=arg_list), + '__slots__': (), + '_fields': tuple(field_names), + '__new__': __new__, + '_make': _make, + '_replace': _replace, + '__repr__': __repr__, + '_asdict': _asdict, + '__getnewargs__': __getnewargs__, + } + for index, name in enumerate(field_names): + doc = 'Alias for field number {index:d}'.format(index=index) + class_namespace[name] = property(_itemgetter(index), doc=doc) + + result = type(typename, (tuple,), class_namespace) # For pickling to work, the __module__ variable needs to be set to the frame # where the named tuple is created. Bypass this step in environments where diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index 3bf15786189b30..dd63d88ca2de15 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -194,7 +194,6 @@ def test_factory(self): self.assertEqual(Point.__module__, __name__) self.assertEqual(Point.__getitem__, tuple.__getitem__) self.assertEqual(Point._fields, ('x', 'y')) - self.assertIn('class Point(tuple)', Point._source) self.assertRaises(ValueError, namedtuple, 'abc%', 'efg ghi') # type has non-alpha char self.assertRaises(ValueError, namedtuple, 'class', 'efg ghi') # type has keyword @@ -404,22 +403,8 @@ class B(A): pass self.assertEqual(repr(B(1)), 'B(x=1)') - def test_source(self): - # verify that _source can be run through exec() - tmp = namedtuple('NTColor', 'red green blue') - globals().pop('NTColor', None) # remove artifacts from other tests - exec(tmp._source, globals()) - self.assertIn('NTColor', globals()) - c = NTColor(10, 20, 30) - self.assertEqual((c.red, c.green, c.blue), (10, 20, 30)) - self.assertEqual(NTColor._fields, ('red', 'green', 'blue')) - globals().pop('NTColor', None) # clean-up after this test - def test_keyword_only_arguments(self): # See issue 25628 - with support.captured_stdout() as template: - NT = namedtuple('NT', ['x', 'y'], verbose=True) - self.assertIn('class NT', NT._source) with self.assertRaises(TypeError): NT = namedtuple('NT', ['x', 'y'], True) From c135a36f14d53417d1e2573d506eaf8b76b701af Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 17 Jul 2017 19:44:19 -0700 Subject: [PATCH 02/10] avoid using closure variables and break up long lines --- Lib/collections/__init__.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 4c55f963c41477..6b2005deda3487 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -370,35 +370,37 @@ def namedtuple(typename, field_names, *, verbose=False, rename=False, module=Non arg_list = repr(tuple(field_names)).replace("'", "")[1:-1] num_fields = len(field_names) - repr_fmt = '(' + ', '.join(_repr_template.format(name=name) for name in field_names) + ')' + repr_fmt = '(' + ', '.join(_repr_template.format(name=name) + for name in field_names) + ')' namespace = {'_tuple': tuple} - exec(_new_template.format(typename=typename, arg_list=arg_list), namespace) + new_source = _new_template.format(typename=typename, arg_list=arg_list) + exec(new_source, namespace) __new__ = namespace['__new__'] @classmethod def _make(cls, iterable, new=tuple.__new__, len=len): result = new(cls, iterable) - if len(result) != num_fields: - raise TypeError('Expected %d arguments, got %d' % (num_fields, len(result))) + if len(result) != cls._num_fields: + raise TypeError('Expected %d arguments, got %d' % + (cls._num_fields, len(result))) return result - _make.__func__.__doc__ = 'Make a new {typename} object from a sequence or iterable'.format(typename=typename) + _make.__func__.__doc__ = ('Make a new {typename} object from a sequence ' + 'or iterable').format(typename=typename) def _replace(_self, **kwds): - result = _self._make(map(kwds.pop, field_names, _self)) + result = _self._make(map(kwds.pop, _self._fields, _self)) if kwds: raise ValueError('Got unexpected field names: %r' % list(kwds)) return result - _replace.__doc__ = ( - 'Return a new {typename} object replacing specified fields with new values'.format( - typename=typename) - ) + _replace.__doc__ = ('Return a new {typename} object replacing specified ' + 'fields with new values').format(typename=typename) def __repr__(self): 'Return a nicely formatted representation string' - return self.__class__.__name__ + repr_fmt % self + return self.__class__.__name__ + self._repr_fmt % self def _asdict(self): 'Return a new OrderedDict which maps field names to their values.' @@ -409,7 +411,8 @@ def __getnewargs__(self): return tuple(self) class_namespace = { - '__doc__': '{typename}({arg_list})'.format(typename=typename, arg_list=arg_list), + '__doc__': '{typename}({arg_list})'.format(typename=typename, + arg_list=arg_list), '__slots__': (), '_fields': tuple(field_names), '__new__': __new__, @@ -418,6 +421,8 @@ def __getnewargs__(self): '__repr__': __repr__, '_asdict': _asdict, '__getnewargs__': __getnewargs__, + '_num_fields': len(field_names), + '_repr_fmt': repr_fmt, } for index, name in enumerate(field_names): doc = 'Alias for field number {index:d}'.format(index=index) From 77be15fa6b52c4e6d5e038a630c1b168f1516ced Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 17 Jul 2017 20:00:17 -0700 Subject: [PATCH 03/10] bring back _source --- Lib/collections/__init__.py | 70 ++++++++++++++++++++++++++++++++++++ Lib/test/test_collections.py | 15 ++++++++ 2 files changed, 85 insertions(+) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 6b2005deda3487..90ed469eef2ad8 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -303,6 +303,55 @@ def __eq__(self, other): ### namedtuple ################################################################################ +# Used only for the _source attribute, not for creating namedtuple classes. +_class_template = """\ +from builtins import property as _property, tuple as _tuple +from operator import itemgetter as _itemgetter +from collections import OrderedDict + +class {typename}(tuple): + '{typename}({arg_list})' + + __slots__ = () + + _fields = {field_names!r} + + def __new__(_cls, {arg_list}): + 'Create new instance of {typename}({arg_list})' + return _tuple.__new__(_cls, ({arg_list})) + + @classmethod + def _make(cls, iterable, new=tuple.__new__, len=len): + 'Make a new {typename} object from a sequence or iterable' + result = new(cls, iterable) + if len(result) != {num_fields:d}: + raise TypeError('Expected {num_fields:d} arguments, got %d' % len(result)) + return result + + def _replace(_self, **kwds): + 'Return a new {typename} object replacing specified fields with new values' + result = _self._make(map(kwds.pop, {field_names!r}, _self)) + if kwds: + raise ValueError('Got unexpected field names: %r' % list(kwds)) + return result + + def __repr__(self): + 'Return a nicely formatted representation string' + return self.__class__.__name__ + '({repr_fmt})' % self + + def _asdict(self): + 'Return a new OrderedDict which maps field names to their values.' + return OrderedDict(zip(self._fields, self)) + + def __getnewargs__(self): + 'Return self as a plain tuple. Used by copy and pickle.' + return tuple(self) + +{field_defs} +""" +_field_template = '''\ + {name} = _property(_itemgetter({index:d}), doc='Alias for field number {index:d}') +''' _repr_template = '{name}=%r' _new_template = ''' def __new__(_cls, {arg_list}): @@ -311,6 +360,23 @@ def __new__(_cls, {arg_list}): ''' +class _source_descriptor: + """Descriptor for generating the _source attribute of a namedtuple.""" + __slots__ = () + + def __get__(self, instance, owner): + class_definition = _class_template.format( + typename = owner.__name__, + field_names = owner._fields, + num_fields = owner._num_fields, + arg_list = repr(owner._fields).replace("'", "")[1:-1], + repr_fmt = owner._repr_fmt, + field_defs = '\n'.join(_field_template.format(index=index, name=name) + for index, name in enumerate(owner._fields)) + ) + return class_definition + + def namedtuple(typename, field_names, *, verbose=False, rename=False, module=None): """Returns a new subclass of tuple with named fields. @@ -423,6 +489,7 @@ def __getnewargs__(self): '__getnewargs__': __getnewargs__, '_num_fields': len(field_names), '_repr_fmt': repr_fmt, + '_source': _source_descriptor(), } for index, name in enumerate(field_names): doc = 'Alias for field number {index:d}'.format(index=index) @@ -430,6 +497,9 @@ def __getnewargs__(self): result = type(typename, (tuple,), class_namespace) + if verbose: + print(result._source) + # For pickling to work, the __module__ variable needs to be set to the frame # where the named tuple is created. Bypass this step in environments where # sys._getframe is not defined (Jython for example) or sys._getframe is not diff --git a/Lib/test/test_collections.py b/Lib/test/test_collections.py index dd63d88ca2de15..3bf15786189b30 100644 --- a/Lib/test/test_collections.py +++ b/Lib/test/test_collections.py @@ -194,6 +194,7 @@ def test_factory(self): self.assertEqual(Point.__module__, __name__) self.assertEqual(Point.__getitem__, tuple.__getitem__) self.assertEqual(Point._fields, ('x', 'y')) + self.assertIn('class Point(tuple)', Point._source) self.assertRaises(ValueError, namedtuple, 'abc%', 'efg ghi') # type has non-alpha char self.assertRaises(ValueError, namedtuple, 'class', 'efg ghi') # type has keyword @@ -403,8 +404,22 @@ class B(A): pass self.assertEqual(repr(B(1)), 'B(x=1)') + def test_source(self): + # verify that _source can be run through exec() + tmp = namedtuple('NTColor', 'red green blue') + globals().pop('NTColor', None) # remove artifacts from other tests + exec(tmp._source, globals()) + self.assertIn('NTColor', globals()) + c = NTColor(10, 20, 30) + self.assertEqual((c.red, c.green, c.blue), (10, 20, 30)) + self.assertEqual(NTColor._fields, ('red', 'green', 'blue')) + globals().pop('NTColor', None) # clean-up after this test + def test_keyword_only_arguments(self): # See issue 25628 + with support.captured_stdout() as template: + NT = namedtuple('NT', ['x', 'y'], verbose=True) + self.assertIn('class NT', NT._source) with self.assertRaises(TypeError): NT = namedtuple('NT', ['x', 'y'], True) From 649bb2e5acf470877f03e6b1709015796a3dd7f7 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 17 Jul 2017 20:50:04 -0700 Subject: [PATCH 04/10] update _source documentation --- Doc/library/collections.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index d6d2056dfc496c..9d1abf86b0fee9 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -880,13 +880,16 @@ field names, the method and attribute names start with an underscore. .. attribute:: somenamedtuple._source - A string with the pure Python source code used to create the named - tuple class. The source makes the named tuple self-documenting. - It can be printed, executed using :func:`exec`, or saved to a file - and imported. + A string with pure Python source code that can be used to create an + equivalent named tuple class. The source makes the named tuple + self-documenting. It can be printed, executed using :func:`exec`, or + saved to a file and imported. .. versionadded:: 3.3 + .. versionchanged:: 3.7 + ``_source`` is no longer used to created the named tuple class. + .. attribute:: somenamedtuple._fields Tuple of strings listing the field names. Useful for introspection From da03fdb9c6d46034874d439be097f688c90af5d0 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 27 Aug 2017 13:37:22 -0700 Subject: [PATCH 05/10] micro-optimizations --- Lib/collections/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 90ed469eef2ad8..1214e1cb3fc20b 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -356,7 +356,7 @@ def __getnewargs__(self): _new_template = ''' def __new__(_cls, {arg_list}): 'Create new instance of {typename}({arg_list})' - return _tuple.__new__(_cls, ({arg_list})) + return _tuple_new(_cls, ({arg_list})) ''' @@ -439,17 +439,17 @@ def namedtuple(typename, field_names, *, verbose=False, rename=False, module=Non repr_fmt = '(' + ', '.join(_repr_template.format(name=name) for name in field_names) + ')' - namespace = {'_tuple': tuple} + namespace = {'_tuple_new': tuple.__new__} new_source = _new_template.format(typename=typename, arg_list=arg_list) exec(new_source, namespace) __new__ = namespace['__new__'] @classmethod - def _make(cls, iterable, new=tuple.__new__, len=len): + def _make(cls, iterable, new=tuple.__new__, len=len, num_fields=num_fields): result = new(cls, iterable) if len(result) != cls._num_fields: raise TypeError('Expected %d arguments, got %d' % - (cls._num_fields, len(result))) + (num_fields, len(result))) return result _make.__func__.__doc__ = ('Make a new {typename} object from a sequence ' From 805e0cdeb20c60190427a04216c618af7dbc1679 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 27 Aug 2017 13:53:45 -0700 Subject: [PATCH 06/10] reword docs --- Doc/library/collections.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst index 9d1abf86b0fee9..1e97b2268f1f83 100644 --- a/Doc/library/collections.rst +++ b/Doc/library/collections.rst @@ -888,7 +888,8 @@ field names, the method and attribute names start with an underscore. .. versionadded:: 3.3 .. versionchanged:: 3.7 - ``_source`` is no longer used to created the named tuple class. + ``_source`` is no longer used to create the named tuple class implementation, but contains + an equivalent implementation. .. attribute:: somenamedtuple._fields From 1f13a26efbd35499f1a1781e3183bf0e0e62091a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 27 Aug 2017 13:53:52 -0700 Subject: [PATCH 07/10] create __new__ docstring separately --- Lib/collections/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 1214e1cb3fc20b..6aaa723c3cf7da 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -355,9 +355,9 @@ def __getnewargs__(self): _repr_template = '{name}=%r' _new_template = ''' def __new__(_cls, {arg_list}): - 'Create new instance of {typename}({arg_list})' return _tuple_new(_cls, ({arg_list})) ''' +_new_doc_template = 'Create new instance of {typename}({arg_list})' class _source_descriptor: @@ -440,9 +440,10 @@ def namedtuple(typename, field_names, *, verbose=False, rename=False, module=Non for name in field_names) + ')' namespace = {'_tuple_new': tuple.__new__} - new_source = _new_template.format(typename=typename, arg_list=arg_list) + new_source = _new_template.format(arg_list=arg_list) exec(new_source, namespace) __new__ = namespace['__new__'] + __new__.__doc__ = _new_doc_template.format(typename=typename, arg_list=arg_list) @classmethod def _make(cls, iterable, new=tuple.__new__, len=len, num_fields=num_fields): From af0c6bf7d51e8315fdb302e2e6ca63dc27b0cc8f Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 27 Aug 2017 13:59:27 -0700 Subject: [PATCH 08/10] set __module__ and __qualname__ --- Lib/collections/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 6aaa723c3cf7da..7ee70f4ede5380 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -477,6 +477,11 @@ def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return tuple(self) + module_name = 'namedtuple_{typename}'.format(typename=typename) + for method in (__new__, _make.__func__, _replace, __repr__, _asdict, __getnewargs__): + method.__module__ = module_name + method.__qualname__ = '{typename}.{name}'.format(typename=typename, name=method.__name__) + class_namespace = { '__doc__': '{typename}({arg_list})'.format(typename=typename, arg_list=arg_list), From c28d4e4643424cf7514e58993fe13940ff0978bf Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 27 Aug 2017 14:03:55 -0700 Subject: [PATCH 09/10] add NEWS entry --- .../next/Library/2017-08-27-14-03-50.bpo-28638.W4CQxG.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2017-08-27-14-03-50.bpo-28638.W4CQxG.rst diff --git a/Misc/NEWS.d/next/Library/2017-08-27-14-03-50.bpo-28638.W4CQxG.rst b/Misc/NEWS.d/next/Library/2017-08-27-14-03-50.bpo-28638.W4CQxG.rst new file mode 100644 index 00000000000000..411314399edae5 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2017-08-27-14-03-50.bpo-28638.W4CQxG.rst @@ -0,0 +1,2 @@ +Speed up namedtuple class creation by 4x by avoiding usage of exec(). Patch +by Jelle Zijlstra. From d73ae9d11cbe71f10d20a2e8908d4526a7533d8a Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Mon, 28 Aug 2017 20:41:22 -0700 Subject: [PATCH 10/10] lots of f-strings --- Lib/collections/__init__.py | 38 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 7ee70f4ede5380..407323d3ecaa32 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -357,7 +357,6 @@ def __getnewargs__(self): def __new__(_cls, {arg_list}): return _tuple_new(_cls, ({arg_list})) ''' -_new_doc_template = 'Create new instance of {typename}({arg_list})' class _source_descriptor: @@ -414,56 +413,54 @@ def namedtuple(typename, field_names, *, verbose=False, rename=False, module=Non or _iskeyword(name) or name.startswith('_') or name in seen): - field_names[index] = '_%d' % index + field_names[index] = f'_{index:d}' 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') if not name.isidentifier(): raise ValueError('Type names and field names must be valid ' - 'identifiers: %r' % name) + f'identifiers: {name!r}') if _iskeyword(name): raise ValueError('Type names and field names cannot be a ' - 'keyword: %r' % name) + f'keyword: {name!r}') seen = set() for name in field_names: if name.startswith('_') and not rename: raise ValueError('Field names cannot start with an underscore: ' - '%r' % name) + f'{name!r}') if name in seen: - raise ValueError('Encountered duplicate field name: %r' % name) + raise ValueError(f'Encountered duplicate field name: {name!r}') seen.add(name) arg_list = repr(tuple(field_names)).replace("'", "")[1:-1] num_fields = len(field_names) - repr_fmt = '(' + ', '.join(_repr_template.format(name=name) - for name in field_names) + ')' + repr_fmt = '(' + ', '.join(f'{name}=%r' for name in field_names) + ')' namespace = {'_tuple_new': tuple.__new__} new_source = _new_template.format(arg_list=arg_list) exec(new_source, namespace) __new__ = namespace['__new__'] - __new__.__doc__ = _new_doc_template.format(typename=typename, arg_list=arg_list) + __new__.__doc__ = f'Create new instance of {typename}({arg_list})' @classmethod def _make(cls, iterable, new=tuple.__new__, len=len, num_fields=num_fields): result = new(cls, iterable) if len(result) != cls._num_fields: - raise TypeError('Expected %d arguments, got %d' % - (num_fields, len(result))) + raise TypeError(f'Expected {num_fields} arguments, got {len(result)}') return result - _make.__func__.__doc__ = ('Make a new {typename} object from a sequence ' - 'or iterable').format(typename=typename) + _make.__func__.__doc__ = (f'Make a new {typename} object from a sequence ' + 'or iterable') def _replace(_self, **kwds): result = _self._make(map(kwds.pop, _self._fields, _self)) if kwds: - raise ValueError('Got unexpected field names: %r' % list(kwds)) + raise ValueError(f'Got unexpected field names: {list(kwds)}') return result - _replace.__doc__ = ('Return a new {typename} object replacing specified ' - 'fields with new values').format(typename=typename) + _replace.__doc__ = (f'Return a new {typename} object replacing specified ' + 'fields with new values') def __repr__(self): 'Return a nicely formatted representation string' @@ -477,14 +474,13 @@ def __getnewargs__(self): 'Return self as a plain tuple. Used by copy and pickle.' return tuple(self) - module_name = 'namedtuple_{typename}'.format(typename=typename) + module_name = f'namedtuple_{typename}' for method in (__new__, _make.__func__, _replace, __repr__, _asdict, __getnewargs__): method.__module__ = module_name - method.__qualname__ = '{typename}.{name}'.format(typename=typename, name=method.__name__) + method.__qualname__ = f'{typename}.{method.__name__}' class_namespace = { - '__doc__': '{typename}({arg_list})'.format(typename=typename, - arg_list=arg_list), + '__doc__': f'{typename}({arg_list})', '__slots__': (), '_fields': tuple(field_names), '__new__': __new__, @@ -498,7 +494,7 @@ def __getnewargs__(self): '_source': _source_descriptor(), } for index, name in enumerate(field_names): - doc = 'Alias for field number {index:d}'.format(index=index) + doc = f'Alias for field number {index:d}' class_namespace[name] = property(_itemgetter(index), doc=doc) result = type(typename, (tuple,), class_namespace)