Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
bpo-37822: Add math.as_integer_ratio().
  • Loading branch information
serhiy-storchaka committed Aug 11, 2019
commit 482b1f983e466f2834c739c6247ff555f5b5cfeb
11 changes: 11 additions & 0 deletions Doc/library/math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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*.
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ New Modules
Improved Modules
================


math
----

Added new function :func:`math.as_integer_ratio`.
(Contributed by Serhiy Storchaka in :issue:``.)


threading
---------

Expand Down
26 changes: 7 additions & 19 deletions Lib/fractions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
18 changes: 1 addition & 17 deletions Lib/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import random
import struct
import sys
from decimal import Decimal
from fractions import Fraction


eps = 1E-05
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added :func:`math.as_integer_ratio`.
11 changes: 10 additions & 1 deletion Modules/clinic/mathmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 74 additions & 0 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Copy Markdown
Contributor

@aeros aeros Aug 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the context, I can roughly tell what this conditional is doing. From my understanding, _PyObject_LookupAttrId() is assessing whether or not the PyObject x contains an as_integer_ratio attribute. If a value less than zero is returned (usually -1), it does not contain that attribute.

However, I'm not certain that I understand where PyId_as_integer_ratio is coming from or how PyId actually works. I was unable to find any documentation on PyId or _Py_IDENTIFIER(), so I'm guessing it's an internal part of the C-API (since it's prefixed with an underscore).

My best guess is that a reference to PyId_as_integer_ratio was created when _Py_IDENTIFIER(as_integer_ratio) was used.

I'm fairly new to the C-API, so I'm trying to learn more about it so that I can be more helpful in PR reviews that involve it. Particularly the internal implementation details that aren't in the documentation.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is described in the header: Include/cpython/object.h.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks for letting me know where to look. The code comments there addressed my question:

PyId_foo is a static variable, either on block level or file level. On first usage, the string "foo" is interned, and the structures are linked.

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},
Expand Down