diff --git a/Lib/fractions.py b/Lib/fractions.py index 2e7047a81844d2..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'] @@ -115,22 +116,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 +142,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 = _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..1831d0a36840b7 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..1cf2e7c66ee9e9 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,34 @@ 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 + 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/Modules/clinic/mathmodule.c.h b/Modules/clinic/mathmodule.c.h index 84561b955787b7..66e9ad2c596f70 100644 --- a/Modules/clinic/mathmodule.c.h +++ b/Modules/clinic/mathmodule.c.h @@ -712,4 +712,16 @@ 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" +"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=7f658f3ee1d866ba input=a9049054013a1b77]*/ diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index e1b46ec384a373..026fc324b9ef0e 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -3306,9 +3306,87 @@ math_comb_impl(PyObject *module, PyObject *n, PyObject *k) } +/*[clinic input] +math._as_integer_ratio + x: object + / + +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=2e4f43d93f6e7850 input=b54b48dd6bbe22ea]*/ +{ + _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},