From b45b672e42ba3d321c742393f7689a8a5810b773 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 1 May 2025 18:42:56 +0300 Subject: [PATCH 1/7] gh-72902: improve Fraction(str) speed (don't use regexp's) Co-authored-by: Wolfgang Maier --- Lib/fractions.py | 88 ++++++++++--------- ...5-05-02-13-30-08.gh-issue-72902.B0BV8C.rst | 2 + 2 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst diff --git a/Lib/fractions.py b/Lib/fractions.py index 8163e3bb594f6b..e42fd03349823d 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -53,20 +53,6 @@ def _hash_algorithm(numerator, denominator): result = hash_ if numerator >= 0 else -hash_ return -2 if result == -1 else result -_RATIONAL_FORMAT = re.compile(r""" - \A\s* # optional whitespace at the start, - (?P[-+]?) # an optional sign, then - (?=\d|\.\d) # lookahead for digit or .digit - (?P\d*|\d+(_\d+)*) # numerator (possibly empty) - (?: # followed by - (?:\s*/\s*(?P\d+(_\d+)*))? # an optional denominator - | # or - (?:\.(?P\d*|\d+(_\d+)*))? # an optional fractional part - (?:E(?P[-+]?\d+(_\d+)*))? # and optional exponent - ) - \s*\z # and optional whitespace to finish -""", re.VERBOSE | re.IGNORECASE) - # Helpers for formatting @@ -238,11 +224,6 @@ def __new__(cls, numerator=0, denominator=None): self._denominator = 1 return self - elif isinstance(numerator, numbers.Rational): - self._numerator = numerator.numerator - self._denominator = numerator.denominator - return self - elif (isinstance(numerator, float) or (not isinstance(numerator, type) and hasattr(numerator, 'as_integer_ratio'))): @@ -252,31 +233,52 @@ def __new__(cls, numerator=0, denominator=None): elif isinstance(numerator, str): # Handle construction from strings. - m = _RATIONAL_FORMAT.match(numerator) - if m is None: - raise ValueError('Invalid literal for Fraction: %r' % - numerator) - numerator = int(m.group('num') or '0') - denom = m.group('denom') - if denom: - denominator = int(denom) - else: - denominator = 1 - decimal = m.group('decimal') - if decimal: - decimal = decimal.replace('_', '') - scale = 10**len(decimal) - numerator = numerator * scale + int(decimal) - denominator *= scale - exp = m.group('exp') - if exp: - exp = int(exp) - if exp >= 0: - numerator *= 10**exp + fraction_literal = numerator + num, _, denom = fraction_literal.partition('/') + try: + num = num.strip() + denom = denom.strip() + if num and denom and denom[0].isdigit(): + denominator = int(denom) + numerator = int(num) + elif num and not _: + denominator = 1 + num, _, exp = num.replace('E', 'e').partition('e') + if _ and not exp: + raise ValueError + num, _, decimal = num.partition('.') + if decimal: + if num and num[0] in ('+', '-'): + sign = num[0] == '-' + num = num[1:] + else: + sign = 0 + numerator = int(num or '0') + decimal_len = len(decimal.replace('_', '')) + decimal = int(decimal) + scale = 10**decimal_len + numerator = numerator*scale + decimal + denominator *= scale + if sign: + numerator = -numerator else: - denominator *= 10**-exp - if m.group('sign') == '-': - numerator = -numerator + numerator = int(num) + if exp: + exp = int(exp) + if exp >= 0: + numerator *= 10**exp + else: + denominator *= 10**-exp + else: + raise ValueError + except ValueError: + raise ValueError('Invalid literal for Fraction: %r' % + fraction_literal) + + elif isinstance(numerator, numbers.Rational): + self._numerator = numerator.numerator + self._denominator = numerator.denominator + return self else: raise TypeError("argument should be a string or a Rational " diff --git a/Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst b/Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst new file mode 100644 index 00000000000000..c6e0d643ec976b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst @@ -0,0 +1,2 @@ +Improve speed (x1.6-x2) of the :class:`~fractions.Fraction` constructor for +string inputs. From d6b008336c6f72469647116b74697ed9b9235e5b Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Wed, 14 May 2025 15:32:54 +0300 Subject: [PATCH 2/7] Update Lib/fractions.py Co-authored-by: Pieter Eendebak --- Lib/fractions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index e42fd03349823d..f4c28aabdbe82c 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -234,14 +234,14 @@ def __new__(cls, numerator=0, denominator=None): elif isinstance(numerator, str): # Handle construction from strings. fraction_literal = numerator - num, _, denom = fraction_literal.partition('/') + num, is_fraction_format, denom = fraction_literal.partition('/') try: num = num.strip() denom = denom.strip() if num and denom and denom[0].isdigit(): denominator = int(denom) numerator = int(num) - elif num and not _: + elif num and not is_fraction_format: denominator = 1 num, _, exp = num.replace('E', 'e').partition('e') if _ and not exp: From 05e9110cd59b9e2c5bfd78c41257138af28c85c8 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Thu, 15 May 2025 09:49:26 +0300 Subject: [PATCH 3/7] + a quick fix --- Lib/fractions.py | 21 ++++++++++++++++----- Lib/test/test_fractions.py | 1 + 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index f4c28aabdbe82c..16e8986fad3589 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -247,23 +247,32 @@ def __new__(cls, numerator=0, denominator=None): if _ and not exp: raise ValueError num, _, decimal = num.partition('.') - if decimal: - if num and num[0] in ('+', '-'): + if num: + if num[0] in ('+', '-'): sign = num[0] == '-' num = num[1:] else: sign = 0 + if num and not (num[-1].isdigit() and num[0].isdigit()): + raise ValueError + else: + sign = 0 + if decimal: + if not decimal[0].isdigit() or not decimal[-1].isdigit(): + raise ValueError numerator = int(num or '0') decimal_len = len(decimal.replace('_', '')) decimal = int(decimal) scale = 10**decimal_len numerator = numerator*scale + decimal denominator *= scale - if sign: - numerator = -numerator else: numerator = int(num) + if sign: + numerator = -numerator if exp: + if not (exp[0] in ('+', '-') or exp[0].isdigit()): + raise ValueError exp = int(exp) if exp >= 0: numerator *= 10**exp @@ -271,7 +280,9 @@ def __new__(cls, numerator=0, denominator=None): denominator *= 10**-exp else: raise ValueError - except ValueError: + except ValueError as exc: + if exc.args and re.match('^Exceeds', exc.args[0]): + raise raise ValueError('Invalid literal for Fraction: %r' % fraction_literal) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index 96b3f30519459b..b7a65ff0f12142 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -434,6 +434,7 @@ def check_invalid(s): # Imitate float's parsing. check_invalid("+ 3/2") check_invalid("- 3/2") + check_invalid("+ 343.33") # Avoid treating '.' as a regex special character. check_invalid("3a2") # Don't accept combinations of decimals and rationals. From 7c567fcd6cbf4da33989f3318ba2f7b3b7cd234a Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 16 May 2025 12:07:13 +0300 Subject: [PATCH 4/7] Update Lib/fractions.py Co-authored-by: Pieter Eendebak --- Lib/fractions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index 16e8986fad3589..92f51766016d7e 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -243,8 +243,8 @@ def __new__(cls, numerator=0, denominator=None): numerator = int(num) elif num and not is_fraction_format: denominator = 1 - num, _, exp = num.replace('E', 'e').partition('e') - if _ and not exp: + num, is_exp_format, exp = num.replace('E', 'e').partition('e') + if is_exp_format and not exp: raise ValueError num, _, decimal = num.partition('.') if num: From e383bdb316d1dc75a19c92a7467fe9fd4c1e37cf Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Mon, 19 May 2025 05:38:17 +0300 Subject: [PATCH 5/7] address review: add test --- Lib/test/test_fractions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index b7a65ff0f12142..31f8593509b0ec 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -446,6 +446,7 @@ def check_invalid(s): # No space around e. check_invalid("3.2 e1") check_invalid("3.2e 1") + check_invalid("232e\t2") # Fractional part don't need a sign. check_invalid("3.+2") check_invalid("3.-2") From 48f73f01b2279258deec0bb06ffb765072097f18 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 20 May 2025 12:01:03 +0300 Subject: [PATCH 6/7] + revert moving numbers.Rational check --- Lib/fractions.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index 92f51766016d7e..cb7fb9a9d4d90f 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -224,6 +224,11 @@ def __new__(cls, numerator=0, denominator=None): self._denominator = 1 return self + elif isinstance(numerator, numbers.Rational): + self._numerator = numerator.numerator + self._denominator = numerator.denominator + return self + elif (isinstance(numerator, float) or (not isinstance(numerator, type) and hasattr(numerator, 'as_integer_ratio'))): @@ -286,11 +291,6 @@ def __new__(cls, numerator=0, denominator=None): raise ValueError('Invalid literal for Fraction: %r' % fraction_literal) - elif isinstance(numerator, numbers.Rational): - self._numerator = numerator.numerator - self._denominator = numerator.denominator - return self - else: raise TypeError("argument should be a string or a Rational " "instance or have the as_integer_ratio() method") From 20436d8800e88b092d0d32ddffe566dbf7e13596 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Tue, 20 May 2025 12:08:44 +0300 Subject: [PATCH 7/7] +1 --- .../next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst b/Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst index c6e0d643ec976b..c4b805c95898aa 100644 --- a/Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst +++ b/Misc/NEWS.d/next/Library/2025-05-02-13-30-08.gh-issue-72902.B0BV8C.rst @@ -1,2 +1,2 @@ -Improve speed (x1.6-x2) of the :class:`~fractions.Fraction` constructor for +Improve speed (x1.3-1.5) of the :class:`~fractions.Fraction` constructor for string inputs.