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..05eda36b438dac 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 number, " + "not %s" % type(numerator).__name__) from None 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},