Skip to content
Closed
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
5 changes: 5 additions & 0 deletions Doc/howto/sorting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,11 @@ library provides several tools that do less work than a full sort:
respectively. These functions make a single pass over the input data and
require almost no auxiliary memory.

* :func:`minmax` returns both the smallest and largest values,
respectively. This functions make a single pass over the input data and
require almost no auxiliary memory. This function is useful if both :meth:`min` and
:meth:`max` need to be called and thus is more efficient for larger datasets.

* :func:`heapq.nsmallest` and :func:`heapq.nlargest` return
the *n* smallest and largest values, respectively. These functions
make a single pass over the data keeping only *n* elements in memory
Expand Down
22 changes: 22 additions & 0 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1295,6 +1295,28 @@ are always available. They are listed here in alphabetical order.
.. versionchanged:: 3.8
The *key* can be ``None``.

.. function:: minmax(iterable, /, *, key=None)
minmax(iterable, /, *, default, key=None)
minmax(arg1, arg2, /, *args, key=None)

Return the smallest and largest items respectively in an iterable or the smallest and largest
of two or more arguments.

If one positional argument is provided, it should be an :term:`iterable`.
The smallest and largest items in the iterable are returned. If two or more positional
arguments are provided, the smallest and largest of the positional arguments are
returned.

There are two optional keyword-only arguments. The *key* argument specifies
a one-argument ordering function like that used for :meth:`list.sort`. The
*default* argument specifies an object to return if the provided iterable is
empty. If the iterable is empty and *default* is not provided, a
:exc:`ValueError` is raised.

If multiple items are minimal / maximal, the function returns the first one
encountered. This is consistent with other sort-stability preserving tools
such as ``(sorted(iterable, key=keyfunc)[0], sorted(iterable, key=keyfunc)[-1])``
and ``(heapq.nsmallest(1,iterable, key=keyfunc), heapq.nlargest(1,iterable, key=keyfunc))``.

.. function:: next(iterator, /)
next(iterator, default, /)
Expand Down
66 changes: 66 additions & 0 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -1630,6 +1630,72 @@ def __getitem__(self, index):
self.assertEqual(min(data, key=f),
sorted(data, key=f)[0])


def test_minmax(self):
self.assertEqual(minmax('123123'), ('1', '3'))
self.assertEqual(minmax(1, 2, 3), (1, 3))
self.assertEqual(minmax((1, 2, 3, 1, 2, 3)), (1, 3))
self.assertEqual(minmax([1, 2, 3, 1, 2, 3]), (1, 3))

self.assertEqual(minmax(1, 2, 3.0), (1, 3.0))
self.assertEqual(minmax(1, 2.0, 3), (1, 3))
self.assertEqual(minmax(1.0, 2, 3), (1.0, 3))

with self.assertRaisesRegex(
TypeError,
'minmax expected at least 1 argument, got 0'
):
minmax()

self.assertRaises(TypeError, minmax, 42)
with self.assertRaisesRegex(
ValueError,
r'minmax\(\) iterable argument is empty'
):
minmax(())
class BadSeq:
def __getitem__(self, index):
raise ValueError
self.assertRaises(ValueError, minmax, BadSeq())

for stmt in (
"minmax(key=int)", # no args
"minmax(default=None)",
"minmax(1, 2, default=None)", # require container for default
"minmax(default=None, key=int)",
"minmax(1, key=int)", # single arg not iterable
"minmax(1, 2, keystone=int)", # wrong keyword
"minmax(1, 2, key=int, abc=int)", # two many keywords
"minmax(1, 2, key=1)", # keyfunc is not callable
):
try:
exec(stmt, globals())
except TypeError:
pass
else:
self.fail(stmt)

self.assertEqual(minmax((1,), key=neg), (1, 1)) # one elem iterable
self.assertEqual(minmax((1,2), key=neg),(2, 1)) # two elem iterable
self.assertEqual(minmax(1, 2, key=neg), (2, 1)) # two elems

self.assertEqual(minmax((), default=None), (None, None)) # zero elem iterable
self.assertEqual(minmax((1,), default=None), (1, 1)) # one elem iterable
self.assertEqual(minmax((1,2), default=None), (1, 2)) # two elem iterable

self.assertEqual(minmax((), default=1, key=neg), (1, 1))
self.assertEqual(minmax((1, 2), default=1, key=neg), (2, 1))

self.assertEqual(minmax((1, 2), key=None), (1, 2))

data = [random.randrange(200) for i in range(100)]
keys = dict((elem, random.randrange(50)) for elem in data)
f = keys.__getitem__

sorted_vals = sorted(data, key=f)
self.assertEqual(minmax(data, key=f),
(sorted_vals[0], sorted_vals[-1]))

def test_next(self):
it = iter(range(2))
self.assertEqual(next(it), 0)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This creates a new builtin function called ``minmax``, which does work similar
to the functions ``min`` and ``max``; however, is more efficient than running
``min`` and then ``max`` and computes the smallest and largest elements of the
iterable in a single pass; rather than 2 passes.
171 changes: 171 additions & 0 deletions Python/bltinmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -2114,6 +2114,176 @@ the provided iterable is empty.\n\
With two or more positional arguments, return the largest argument.");


static PyObject *
builtin_minmax(PyObject *self, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *it = NULL, *item, *val, *maxitem, *maxval, *minitem, *minval, *minmax_obj, *keyfunc=NULL;
PyObject *defaultval = NULL;
static const char * const keywords[] = {"key", "default", NULL};
static _PyArg_Parser _parser = {"|$OO:minmax", keywords, 0};

if (nargs == 0) {
PyErr_Format(PyExc_TypeError, "minmax expected at least 1 argument, got 0");
return NULL;
}

if (kwnames != NULL && !_PyArg_ParseStackAndKeywords(args + nargs, 0, kwnames, &_parser,
&keyfunc, &defaultval)) {
return NULL;
}

const int positional = nargs > 1; // False iff nargs == 1
if (positional && defaultval != NULL) {
PyErr_Format(PyExc_TypeError,
"Cannot specify a default for minmax() with multiple "
"positional arguments");
return NULL;
}

if (!positional) {
it = PyObject_GetIter(args[0]);
if (it == NULL) {
return NULL;
}
}

if (keyfunc == Py_None) {
keyfunc = NULL;
}

maxitem = NULL; /* the max result */
maxval = NULL; /* the value associated with the max result */

minitem = NULL; /* the min result */
minval = NULL; /* the value associated with the min result */

while (1) {
if (it == NULL) {
if (nargs-- <= 0) {
break;
}
item = *args++;
Py_INCREF(item);
}
else {
item = PyIter_Next(it);
if (item == NULL) {
if (PyErr_Occurred()) {
goto Fail_it;
}
break;
}
}

/* get the value from the key function */
if (keyfunc != NULL) {
val = PyObject_CallOneArg(keyfunc, item);
if (val == NULL)
goto Fail_it_item;
}
/* no key function; the value is the item */
else {
val = Py_NewRef(item);
}

/* minimum/maximum value and item are unset; set them */
if (maxval == NULL || minval == NULL) {
maxitem = item;
maxval = val;

minitem = Py_NewRef(item);
minval = Py_NewRef(val);
}
/* minimum/maximum value and item are set; update them as necessary */
else {
/* check for new minimum value */
const int cmp_lt = PyObject_RichCompareBool(val, minval, Py_LT);

if (cmp_lt < 0) {
goto Fail_it_item_and_val;
}

if (cmp_lt > 0) {
Py_DECREF(minitem);
Py_DECREF(minval);

minval = val;
minitem = item;
} else {
/* Since we did not get a new minimum it could be a new maximum instead */
const int cmp_gt = PyObject_RichCompareBool(val, maxval, Py_GT);

if (cmp_gt < 0) {
goto Fail_it_item_and_val;
}

if(cmp_gt > 0) {
Py_DECREF(maxitem);
Py_DECREF(maxval);

maxval = val;
maxitem = item;
}
else {
Py_DECREF(item);
Py_DECREF(val);
}
}
}
}
if (maxval == NULL || minval == NULL) {
assert(maxitem == NULL);
assert(minitem == NULL);
if (defaultval != NULL) {
maxitem = Py_NewRef(defaultval);
minitem = Py_NewRef(defaultval);
} else {
PyErr_Format(PyExc_ValueError,
"minmax() iterable argument is empty");

goto Fail_it;
}
}else {
Py_DECREF(maxval);
Py_DECREF(minval);
}

Py_XDECREF(it);

if ((minmax_obj = PyTuple_New(2)) == NULL) {
goto Fail_it;
}

PyTuple_SET_ITEM(minmax_obj, 0, minitem);
PyTuple_SET_ITEM(minmax_obj, 1, maxitem);

return minmax_obj;

Fail_it_item_and_val:
Py_DECREF(val);
Fail_it_item:
Py_DECREF(item);
Fail_it:
Py_XDECREF(maxval);
Py_XDECREF(maxitem);

Py_XDECREF(minval);
Py_XDECREF(minitem);

Py_XDECREF(it);
return NULL;
}

PyDoc_STRVAR(minmax_doc,
"minmax(iterable, *[, default=obj, key=func]) -> (min_value, max_value)\n\
minmax(arg1, arg2, *args, *[, key=func]) -> (min_value, max_value)\n\
\n\
With a single iterable argument, return both its smallest and biggest item. The\n\
default keyword-only argument specifies an object to return if\n\
the provided iterable is empty.\n\
With two or more positional arguments, return the smallest and largest argument.");


/*[clinic input]
oct as builtin_oct

Expand Down Expand Up @@ -3392,6 +3562,7 @@ static PyMethodDef builtin_methods[] = {
BUILTIN_LOCALS_METHODDEF
{"max", _PyCFunction_CAST(builtin_max), METH_FASTCALL | METH_KEYWORDS, max_doc},
{"min", _PyCFunction_CAST(builtin_min), METH_FASTCALL | METH_KEYWORDS, min_doc},
{"minmax", _PyCFunction_CAST(builtin_minmax), METH_FASTCALL | METH_KEYWORDS, minmax_doc},
{"next", _PyCFunction_CAST(builtin_next), METH_FASTCALL, next_doc},
BUILTIN_ANEXT_METHODDEF
BUILTIN_OCT_METHODDEF
Expand Down
Loading