From 482b1f983e466f2834c739c6247ff555f5b5cfeb Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 11 Aug 2019 16:55:59 +0300 Subject: [PATCH 1/4] bpo-37822: Add math.as_integer_ratio(). --- Doc/library/math.rst | 11 +++ Doc/whatsnew/3.9.rst | 8 ++ Lib/fractions.py | 26 ++----- Lib/statistics.py | 18 +---- Lib/test/test_math.py | 29 ++++++++ .../2019-08-11-16-55-46.bpo-37822.7xHl1w.rst | 1 + Modules/clinic/mathmodule.c.h | 11 ++- Modules/mathmodule.c | 74 +++++++++++++++++++ 8 files changed, 141 insertions(+), 37 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst diff --git a/Doc/library/math.rst b/Doc/library/math.rst index 43eaba935a164a..bbc577617a9473 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -29,6 +29,17 @@ noted otherwise, all return values are floats. Number-theoretic and representation functions --------------------------------------------- +.. function:: as_integer_ratio(number) + + Return integer ratio. + + This function returns ``number.as_integer_ratio()`` if the argument has + the ``as_integer_ratio()`` method, otherwise it returns a pair + ``(number.numerator, number.denominator)``. + + .. versionadded:: 3.9 + + .. function:: ceil(x) Return the ceiling of *x*, the smallest integer greater than or equal to *x*. diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 61d9e745e87ccf..acdd418c08b2b7 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -109,6 +109,14 @@ New Modules Improved Modules ================ + +math +---- + +Added new function :func:`math.as_integer_ratio`. +(Contributed by Serhiy Storchaka in :issue:``.) + + threading --------- diff --git a/Lib/fractions.py b/Lib/fractions.py index 7443bd3e0c6af9..9f1ff0a10cc4dd 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -115,22 +115,7 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): self = super(Fraction, cls).__new__(cls) if denominator is None: - if type(numerator) is int: - self._numerator = numerator - self._denominator = 1 - return self - - elif isinstance(numerator, numbers.Rational): - self._numerator = numerator.numerator - self._denominator = numerator.denominator - return self - - elif isinstance(numerator, (float, Decimal)): - # Exact conversion - self._numerator, self._denominator = numerator.as_integer_ratio() - return self - - elif isinstance(numerator, str): + if isinstance(numerator, str): # Handle construction from strings. m = _RATIONAL_FORMAT.match(numerator) if m is None: @@ -156,10 +141,13 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): denominator *= 10**-exp if m.group('sign') == '-': numerator = -numerator - else: - raise TypeError("argument should be a string " - "or a Rational instance") + try: + self._numerator, self._denominator = math.as_integer_ratio(numerator) + return self + except TypeError: + raise TypeError("argument should be a string " + "or a Rational instance") elif type(numerator) is int is type(denominator): pass # *very* normal case diff --git a/Lib/statistics.py b/Lib/statistics.py index 77291dd62cb90e..b11047cb703421 100644 --- a/Lib/statistics.py +++ b/Lib/statistics.py @@ -225,27 +225,11 @@ def _exact_ratio(x): x is expected to be an int, Fraction, Decimal or float. """ try: - # Optimise the common case of floats. We expect that the most often - # used numeric type will be builtin floats, so try to make this as - # fast as possible. - if type(x) is float or type(x) is Decimal: - return x.as_integer_ratio() - try: - # x may be an int, Fraction, or Integral ABC. - return (x.numerator, x.denominator) - except AttributeError: - try: - # x may be a float or Decimal subclass. - return x.as_integer_ratio() - except AttributeError: - # Just give up? - pass + return math.as_integer_ratio(x) except (OverflowError, ValueError): # float NAN or INF. assert not _isfinite(x) return (x, None) - msg = "can't convert type '{}' to numerator/denominator" - raise TypeError(msg.format(type(x).__name__)) def _convert(value, T): diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index c237bc1942e655..492e024250bb11 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -12,6 +12,8 @@ import random import struct import sys +from decimal import Decimal +from fractions import Fraction eps = 1E-05 @@ -293,6 +295,33 @@ def testAcosh(self): self.assertRaises(ValueError, math.acosh, NINF) self.assertTrue(math.isnan(math.acosh(NAN))) + def testAsIntegerRatio(self): + as_integer_ratio = math.as_integer_ratio + self.assertEqual(as_integer_ratio(0), (0, 1)) + self.assertEqual(as_integer_ratio(3), (3, 1)) + self.assertEqual(as_integer_ratio(-3), (-3, 1)) + self.assertEqual(as_integer_ratio(False), (0, 1)) + self.assertEqual(as_integer_ratio(True), (1, 1)) + self.assertEqual(as_integer_ratio(0.0), (0, 1)) + self.assertEqual(as_integer_ratio(-0.0), (0, 1)) + self.assertEqual(as_integer_ratio(0.875), (7, 8)) + self.assertEqual(as_integer_ratio(-0.875), (-7, 8)) + self.assertEqual(as_integer_ratio(Decimal('0')), (0, 1)) + self.assertEqual(as_integer_ratio(Decimal('0.875')), (7, 8)) + self.assertEqual(as_integer_ratio(Decimal('-0.875')), (-7, 8)) + self.assertEqual(as_integer_ratio(Fraction(0)), (0, 1)) + self.assertEqual(as_integer_ratio(Fraction(7, 8)), (7, 8)) + self.assertEqual(as_integer_ratio(Fraction(-7, 8)), (-7, 8)) + + self.assertRaises(OverflowError, as_integer_ratio, float('inf')) + self.assertRaises(OverflowError, as_integer_ratio, float('-inf')) + self.assertRaises(ValueError, as_integer_ratio, float('nan')) + self.assertRaises(OverflowError, as_integer_ratio, Decimal('inf')) + self.assertRaises(OverflowError, as_integer_ratio, Decimal('-inf')) + self.assertRaises(ValueError, as_integer_ratio, Decimal('nan')) + + self.assertRaises(TypeError, as_integer_ratio, '0') + def testAsin(self): self.assertRaises(TypeError, math.asin) self.ftest('asin(-1)', math.asin(-1), -math.pi/2) diff --git a/Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst b/Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst new file mode 100644 index 00000000000000..37e9c819b3bb21 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst @@ -0,0 +1 @@ +Added :func:`math.as_integer_ratio`. diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index 84561b955787b7..701e1c932b83f2 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -712,4 +712,13 @@ math_comb(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=f93cfe13ab2fdb4e input=a9049054013a1b77]*/ + +PyDoc_STRVAR(math_as_integer_ratio__doc__, +"as_integer_ratio($module, x, /)\n" +"--\n" +"\n" +"greatest common divisor of x and y"); + +#define MATH_AS_INTEGER_RATIO_METHODDEF \ + {"as_integer_ratio", (PyCFunction)math_as_integer_ratio, METH_O, math_as_integer_ratio__doc__}, +/*[clinic end generated code: output=f9722232fc321d17 input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index e1b46ec384a373..76bc1cc1c9ed4f 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -3306,9 +3306,83 @@ math_comb_impl(PyObject *module, PyObject *n, PyObject *k) } +/*[clinic input] +math.as_integer_ratio + x: object + / +greatest common divisor of x and y +[clinic start generated code]*/ + +static PyObject * +math_as_integer_ratio(PyObject *module, PyObject *x) +/*[clinic end generated code: output=1844868fd4efb2f1 input=d7f2e8ffd51c6599]*/ +{ + _Py_IDENTIFIER(as_integer_ratio); + _Py_IDENTIFIER(numerator); + _Py_IDENTIFIER(denominator); + PyObject *ratio, *as_integer_ratio, *numerator, *denominator; + + if (PyLong_CheckExact(x)) { + return PyTuple_Pack(2, x, _PyLong_One); + } + + if (_PyObject_LookupAttrId(x, &PyId_as_integer_ratio, &as_integer_ratio) < 0) { + return NULL; + } + if (as_integer_ratio) { + ratio = _PyObject_CallNoArg(as_integer_ratio); + Py_DECREF(as_integer_ratio); + if (ratio == NULL) { + return NULL; + } + if (!PyTuple_Check(ratio)) { + PyErr_Format(PyExc_TypeError, + "unexpected return type from as_integer_ratio(): " + "expected tuple, got '%.200s'", + Py_TYPE(ratio)->tp_name); + Py_DECREF(ratio); + return NULL; + } + if (PyTuple_GET_SIZE(ratio) != 2) { + PyErr_SetString(PyExc_ValueError, + "as_integer_ratio() must return a 2-tuple"); + Py_DECREF(ratio); + return NULL; + } + } + else { + if (_PyObject_LookupAttrId(x, &PyId_numerator, &numerator) < 0) { + return NULL; + } + if (numerator == NULL) { + PyErr_Format(PyExc_TypeError, + "required a number, not '%.200s'", + Py_TYPE(x)->tp_name); + return NULL; + } + if (_PyObject_LookupAttrId(x, &PyId_denominator, &denominator) < 0) { + Py_DECREF(numerator); + return NULL; + } + if (denominator == NULL) { + Py_DECREF(numerator); + PyErr_Format(PyExc_TypeError, + "required a number, not '%.200s'", + Py_TYPE(x)->tp_name); + return NULL; + } + ratio = PyTuple_Pack(2, numerator, denominator); + Py_DECREF(numerator); + Py_DECREF(denominator); + } + return ratio; +} + + static PyMethodDef math_methods[] = { {"acos", math_acos, METH_O, math_acos_doc}, {"acosh", math_acosh, METH_O, math_acosh_doc}, + MATH_AS_INTEGER_RATIO_METHODDEF {"asin", math_asin, METH_O, math_asin_doc}, {"asinh", math_asinh, METH_O, math_asinh_doc}, {"atan", math_atan, METH_O, math_atan_doc}, From a587d7d890f137f196ea2de650d324889d5eac5d Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 11 Aug 2019 17:37:25 +0300 Subject: [PATCH 2/4] Better TypeError message. --- Lib/fractions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index 9f1ff0a10cc4dd..05eda36b438dac 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -146,8 +146,8 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): self._numerator, self._denominator = math.as_integer_ratio(numerator) return self except TypeError: - raise TypeError("argument should be a string " - "or a Rational instance") + raise TypeError("argument should be a string or a number, " + "not %s" % type(numerator).__name__) from None elif type(numerator) is int is type(denominator): pass # *very* normal case From e658d19083b3d5abfbbe536e3227e3781883e22a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 19 Aug 2019 08:13:03 +0300 Subject: [PATCH 3/4] Make it a private helper math._as_integer_ratio(). --- Doc/library/math.rst | 11 ----------- Doc/whatsnew/3.9.rst | 8 -------- Lib/fractions.py | 2 +- Lib/statistics.py | 2 +- Lib/test/test_math.py | 3 ++- .../2019-08-11-16-55-46.bpo-37822.7xHl1w.rst | 1 - Modules/clinic/mathmodule.c.h | 15 +++++++++------ Modules/mathmodule.c | 14 +++++++++----- 8 files changed, 22 insertions(+), 34 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst diff --git a/Doc/library/math.rst b/Doc/library/math.rst index bbc577617a9473..43eaba935a164a 100644 --- a/Doc/library/math.rst +++ b/Doc/library/math.rst @@ -29,17 +29,6 @@ noted otherwise, all return values are floats. Number-theoretic and representation functions --------------------------------------------- -.. function:: as_integer_ratio(number) - - Return integer ratio. - - This function returns ``number.as_integer_ratio()`` if the argument has - the ``as_integer_ratio()`` method, otherwise it returns a pair - ``(number.numerator, number.denominator)``. - - .. versionadded:: 3.9 - - .. function:: ceil(x) Return the ceiling of *x*, the smallest integer greater than or equal to *x*. diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index 4dc9613f70e9fe..f09e09c2b90560 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -109,14 +109,6 @@ New Modules Improved Modules ================ - -math ----- - -Added new function :func:`math.as_integer_ratio`. -(Contributed by Serhiy Storchaka in :issue:``.) - - threading --------- diff --git a/Lib/fractions.py b/Lib/fractions.py index 34e04cc16c5ad1..e5b16c9fe358e2 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -143,7 +143,7 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): numerator = -numerator else: try: - self._numerator, self._denominator = math.as_integer_ratio(numerator) + self._numerator, self._denominator = math._as_integer_ratio(numerator) return self except TypeError: raise TypeError("argument should be a string or a number, " diff --git a/Lib/statistics.py b/Lib/statistics.py index b11047cb703421..1831d0a36840b7 100644 --- a/Lib/statistics.py +++ b/Lib/statistics.py @@ -225,7 +225,7 @@ def _exact_ratio(x): x is expected to be an int, Fraction, Decimal or float. """ try: - return math.as_integer_ratio(x) + return math._as_integer_ratio(x) except (OverflowError, ValueError): # float NAN or INF. assert not _isfinite(x) diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index 492e024250bb11..1cf2e7c66ee9e9 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -295,8 +295,9 @@ def testAcosh(self): self.assertRaises(ValueError, math.acosh, NINF) self.assertTrue(math.isnan(math.acosh(NAN))) + @support.cpython_only def testAsIntegerRatio(self): - as_integer_ratio = math.as_integer_ratio + as_integer_ratio = math._as_integer_ratio self.assertEqual(as_integer_ratio(0), (0, 1)) self.assertEqual(as_integer_ratio(3), (3, 1)) self.assertEqual(as_integer_ratio(-3), (-3, 1)) diff --git a/Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst b/Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst deleted file mode 100644 index 37e9c819b3bb21..00000000000000 --- a/Misc/NEWS.d/next/Library/2019-08-11-16-55-46.bpo-37822.7xHl1w.rst +++ /dev/null @@ -1 +0,0 @@ -Added :func:`math.as_integer_ratio`. diff --git a/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index 701e1c932b83f2..66e9ad2c596f70 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -713,12 +713,15 @@ math_comb(PyObject *module, PyObject *const *args, Py_ssize_t nargs) return return_value; } -PyDoc_STRVAR(math_as_integer_ratio__doc__, -"as_integer_ratio($module, x, /)\n" +PyDoc_STRVAR(math__as_integer_ratio__doc__, +"_as_integer_ratio($module, x, /)\n" "--\n" "\n" -"greatest common divisor of x and y"); +"Return integer ratio.\n" +"\n" +"Return a pair of integers, whose ratio is exactly equal to the original\n" +"number and with a positive denominator."); -#define MATH_AS_INTEGER_RATIO_METHODDEF \ - {"as_integer_ratio", (PyCFunction)math_as_integer_ratio, METH_O, math_as_integer_ratio__doc__}, -/*[clinic end generated code: output=f9722232fc321d17 input=a9049054013a1b77]*/ +#define MATH__AS_INTEGER_RATIO_METHODDEF \ + {"_as_integer_ratio", (PyCFunction)math__as_integer_ratio, METH_O, math__as_integer_ratio__doc__}, +/*[clinic end generated code: output=7f658f3ee1d866ba input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index 76bc1cc1c9ed4f..026fc324b9ef0e 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -3307,15 +3307,19 @@ math_comb_impl(PyObject *module, PyObject *n, PyObject *k) /*[clinic input] -math.as_integer_ratio +math._as_integer_ratio x: object / -greatest common divisor of x and y + +Return integer ratio. + +Return a pair of integers, whose ratio is exactly equal to the original +number and with a positive denominator. [clinic start generated code]*/ static PyObject * -math_as_integer_ratio(PyObject *module, PyObject *x) -/*[clinic end generated code: output=1844868fd4efb2f1 input=d7f2e8ffd51c6599]*/ +math__as_integer_ratio(PyObject *module, PyObject *x) +/*[clinic end generated code: output=2e4f43d93f6e7850 input=b54b48dd6bbe22ea]*/ { _Py_IDENTIFIER(as_integer_ratio); _Py_IDENTIFIER(numerator); @@ -3382,7 +3386,7 @@ math_as_integer_ratio(PyObject *module, PyObject *x) static PyMethodDef math_methods[] = { {"acos", math_acos, METH_O, math_acos_doc}, {"acosh", math_acosh, METH_O, math_acosh_doc}, - MATH_AS_INTEGER_RATIO_METHODDEF + MATH__AS_INTEGER_RATIO_METHODDEF {"asin", math_asin, METH_O, math_asin_doc}, {"asinh", math_asinh, METH_O, math_asinh_doc}, {"atan", math_atan, METH_O, math_atan_doc}, From 86f9f20a4b6ea2219475e9564be28ba7dc1ce933 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 20 Aug 2019 09:27:04 +0300 Subject: [PATCH 4/4] Optimize. --- Lib/fractions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/fractions.py b/Lib/fractions.py index e5b16c9fe358e2..9787974a5ed65a 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -9,6 +9,7 @@ import operator import re import sys +from math import _as_integer_ratio __all__ = ['Fraction', 'gcd'] @@ -143,7 +144,7 @@ def __new__(cls, numerator=0, denominator=None, *, _normalize=True): numerator = -numerator else: try: - self._numerator, self._denominator = math._as_integer_ratio(numerator) + self._numerator, self._denominator = _as_integer_ratio(numerator) return self except TypeError: raise TypeError("argument should be a string or a number, "