Skip to content

bpo-35606: Implement math.prod #11359

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 12 additions & 0 deletions Doc/library/math.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ Number-theoretic and representation functions
of *x* and are floats.


.. function:: prod(iterable, *, start=1)

Calculate the product of all the elements in the input *iterable*.
The default *start* value for the product is ``1``.

When the iterable is empty, return the start value. This function is
intended specifically for use with numeric values and may reject
non-numeric types.

.. versionadded:: 3.8


.. function:: remainder(x, y)

Return the IEEE 754-style remainder of *x* with respect to *y*. For
Expand Down
9 changes: 9 additions & 0 deletions Doc/whatsnew/3.8.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ json.tool
Add option ``--json-lines`` to parse every input line as separate JSON object.
(Contributed by Weipeng Hong in :issue:`31553`.)


math
----

Added new function, :func:`math.prod`, as analogous function to :func:`sum`
that returns the product of a 'start' value (default: 1) times an iterable of
numbers. (Contributed by Pablo Galindo in :issue:`issue35606`)


os.path
-------

Expand Down
31 changes: 31 additions & 0 deletions Lib/test/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,37 @@ def test_fractions(self):
self.assertAllClose(fraction_examples, rel_tol=1e-8)
self.assertAllNotClose(fraction_examples, rel_tol=1e-9)

def test_prod(self):
prod = math.prod
self.assertEqual(prod([]), 1)
self.assertEqual(prod([], start=5), 5)
self.assertEqual(prod(list(range(2,8))), 5040)
self.assertEqual(prod(iter(list(range(2,8)))), 5040)
self.assertEqual(prod(range(1, 10), start=10), 3628800)

self.assertEqual(prod([1, 2, 3, 4, 5]), 120)
self.assertEqual(prod([1.0, 2.0, 3.0, 4.0, 5.0]), 120.0)
self.assertEqual(prod([1, 2, 3, 4.0, 5.0]), 120.0)
self.assertEqual(prod([1.0, 2.0, 3.0, 4, 5]), 120.0)

# Test overflow in fast-path for integers
self.assertEqual(prod([1, 1, 2**32, 1, 1]), 2**32)
# Test overflow in fast-path for floats
self.assertEqual(prod([1.0, 1.0, 2**32, 1, 1]), float(2**32))

self.assertRaises(TypeError, prod)
self.assertRaises(TypeError, prod, 42)
self.assertRaises(TypeError, prod, ['a', 'b', 'c'])
self.assertRaises(TypeError, prod, ['a', 'b', 'c'], '')
self.assertRaises(TypeError, prod, [b'a', b'c'], b'')
values = [bytearray(b'a'), bytearray(b'b')]
self.assertRaises(TypeError, prod, values, bytearray(b''))
self.assertRaises(TypeError, prod, [[1], [2], [3]])
self.assertRaises(TypeError, prod, [{2:3}])
self.assertRaises(TypeError, prod, [{2:3}]*2, {2:3})
self.assertRaises(TypeError, prod, [[1], [2], [3]], [])
with self.assertRaises(TypeError):
prod([10, 20], [30, 40]) # start is a keyword-only argument
Comment on lines +1748 to +1757
Copy link

@s-cork s-cork Sep 23, 2021

Choose a reason for hiding this comment

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

@rhettinger / @pablogsal - Skulpt committer here - implementing these tests in our own project.

I was wondering if the intention wasn't quite right with some of these TypeError tests?
Most of them fail because start is a keyword only argument.
Whereas it looks like - for most of them except the final test - the intention is to check multiplication across incompatible types within the prod function.


def test_main():
from doctest import DocFileSuite
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Implement :func:`math.prod` as analogous function to :func:`sum` that
returns the product of a 'start' value (default: 1) times an iterable of
numbers. Patch by Pablo Galindo.
39 changes: 38 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.

167 changes: 167 additions & 0 deletions Modules/mathmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2494,6 +2494,172 @@ math_isclose_impl(PyObject *module, double a, double b, double rel_tol,
}


/*[clinic input]
math.prod

iterable: object
/
*
start: object(c_default="NULL") = 1

Calculate the product of all the elements in the input iterable.

The default start value for the product is 1.

When the iterable is empty, return the start value. This function is
intended specifically for use with numeric values and may reject
non-numeric types.
[clinic start generated code]*/

static PyObject *
math_prod_impl(PyObject *module, PyObject *iterable, PyObject *start)
/*[clinic end generated code: output=36153bedac74a198 input=4c5ab0682782ed54]*/
{
PyObject *result = start;
PyObject *temp, *item, *iter;

iter = PyObject_GetIter(iterable);
if (iter == NULL) {
return NULL;
}

if (result == NULL) {
result = PyLong_FromLong(1);
Copy link
Member

Choose a reason for hiding this comment

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

You can use _PyLong_One.

if (result == NULL) {
Py_DECREF(iter);
return NULL;
}
} else {
Py_INCREF(result);
}
#ifndef SLOW_PROD
/* Fast paths for integers keeping temporary products in C.
* Assumes all inputs are the same type.
* If the assumption fails, default to use PyObjects instead.
*/
if (PyLong_CheckExact(result)) {
int overflow;
long i_result = PyLong_AsLongAndOverflow(result, &overflow);
/* If this already overflowed, don't even enter the loop. */
if (overflow == 0) {
Py_DECREF(result);
result = NULL;
}
/* Loop over all the items in the iterable until we finish, we overflow
* or we found a non integer element */
while(result == NULL) {
Copy link
Member

Choose a reason for hiding this comment

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

Add a space between while and (.

item = PyIter_Next(iter);
if (item == NULL) {
Py_DECREF(iter);
if (PyErr_Occurred()) {
return NULL;
}
return PyLong_FromLong(i_result);
}
if (PyLong_CheckExact(item)) {
long b = PyLong_AsLongAndOverflow(item, &overflow);
long x = i_result * b;
Copy link
Member

Choose a reason for hiding this comment

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

Signed integer overflow is an undefined behavior.

/* Continue if there is no overflow */
if (overflow == 0
&& x < INT_MAX && x > INT_MIN
&& !(b != 0 && x / i_result != b)) {
Copy link
Member

Choose a reason for hiding this comment

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

Possible division by 0.

i_result = x;
Py_DECREF(item);
continue;
}
}
/* Either overflowed or is not an int.
* Restore real objects and process normally */
result = PyLong_FromLong(i_result);
if (result == NULL) {
Py_DECREF(item);
Py_DECREF(iter);
return NULL;
}
temp = PyNumber_Multiply(result, item);
Py_DECREF(result);
Py_DECREF(item);
result = temp;
if (result == NULL) {
Py_DECREF(iter);
return NULL;
}
}
}

/* Fast paths for floats keeping temporary products in C.
* Assumes all inputs are the same type.
* If the assumption fails, default to use PyObjects instead.
*/
if (PyFloat_CheckExact(result)) {
double f_result = PyFloat_AS_DOUBLE(result);
Py_DECREF(result);
result = NULL;
while(result == NULL) {
Copy link
Member

Choose a reason for hiding this comment

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

Add a space between while and (.

item = PyIter_Next(iter);
if (item == NULL) {
Py_DECREF(iter);
if (PyErr_Occurred()) {
return NULL;
}
return PyFloat_FromDouble(f_result);
}
if (PyFloat_CheckExact(item)) {
f_result *= PyFloat_AS_DOUBLE(item);
Py_DECREF(item);
continue;
}
if (PyLong_CheckExact(item)) {
long value;
int overflow;
value = PyLong_AsLongAndOverflow(item, &overflow);
if (!overflow) {
f_result *= (double)value;
Py_DECREF(item);
continue;
}
}
result = PyFloat_FromDouble(f_result);
if (result == NULL) {
Py_DECREF(item);
Py_DECREF(iter);
return NULL;
}
temp = PyNumber_Multiply(result, item);
Py_DECREF(result);
Py_DECREF(item);
result = temp;
if (result == NULL) {
Py_DECREF(iter);
return NULL;
}
}
}
#endif
/* Consume rest of the iterable (if any) that could not be handled
* by specialized functions above.*/
for(;;) {
item = PyIter_Next(iter);
if (item == NULL) {
/* error, or end-of-sequence */
if (PyErr_Occurred()) {
Py_DECREF(result);
result = NULL;
}
break;
}
temp = PyNumber_Multiply(result, item);
Py_DECREF(result);
Py_DECREF(item);
result = temp;
if (result == NULL)
break;
}
Py_DECREF(iter);
return result;
}


static PyMethodDef math_methods[] = {
{"acos", math_acos, METH_O, math_acos_doc},
{"acosh", math_acosh, METH_O, math_acosh_doc},
Expand Down Expand Up @@ -2541,6 +2707,7 @@ static PyMethodDef math_methods[] = {
{"tan", math_tan, METH_O, math_tan_doc},
{"tanh", math_tanh, METH_O, math_tanh_doc},
MATH_TRUNC_METHODDEF
MATH_PROD_METHODDEF
{NULL, NULL} /* sentinel */
};

Expand Down