SF patch #1077353: add key= argument to min and max

(First draft of patch contributed by Steven Bethard.)
This commit is contained in:
Raymond Hettinger 2004-12-03 08:30:39 +00:00
parent e8fdc4502f
commit 3b0c7c20a1
5 changed files with 164 additions and 45 deletions

View file

@ -642,16 +642,28 @@ class C:
of sequence; the result is always a list. of sequence; the result is always a list.
\end{funcdesc} \end{funcdesc}
\begin{funcdesc}{max}{s\optional{, args...}} \begin{funcdesc}{max}{s\optional{, args...}\optional{key}}
With a single argument \var{s}, return the largest item of a With a single argument \var{s}, return the largest item of a
non-empty sequence (such as a string, tuple or list). With more non-empty sequence (such as a string, tuple or list). With more
than one argument, return the largest of the arguments. than one argument, return the largest of the arguments.
The optional \var{key} argument specifies a one argument ordering
function like that used for \method{list.sort()}. The \var{key}
argument, if supplied, must be in keyword form (for example,
\samp{max(a,b,c,key=func)}).
\versionchanged[Added support for the optional \var{key} argument]{2.5}
\end{funcdesc} \end{funcdesc}
\begin{funcdesc}{min}{s\optional{, args...}} \begin{funcdesc}{min}{s\optional{, args...}}
With a single argument \var{s}, return the smallest item of a With a single argument \var{s}, return the smallest item of a
non-empty sequence (such as a string, tuple or list). With more non-empty sequence (such as a string, tuple or list). With more
than one argument, return the smallest of the arguments. than one argument, return the smallest of the arguments.
The optional \var{key} argument specifies a one argument ordering
function like that used for \method{list.sort()}. The \var{key}
argument, if supplied, must be in keyword form (for example,
\samp{min(a,b,c,key=func)}).
\versionchanged[Added support for the optional \var{key} argument]{2.5}
\end{funcdesc} \end{funcdesc}
\begin{funcdesc}{object}{} \begin{funcdesc}{object}{}

View file

@ -1,7 +1,8 @@
# Python test set -- built-in functions # Python test set -- built-in functions
import test.test_support, unittest import test.test_support, unittest
from test.test_support import fcmp, have_unicode, TESTFN, unlink from test.test_support import fcmp, have_unicode, TESTFN, unlink, run_unittest
from operator import neg
import sys, warnings, cStringIO, random, UserDict import sys, warnings, cStringIO, random, UserDict
warnings.filterwarnings("ignore", "hex../oct.. of negative int", warnings.filterwarnings("ignore", "hex../oct.. of negative int",
@ -9,6 +10,10 @@ warnings.filterwarnings("ignore", "hex../oct.. of negative int",
warnings.filterwarnings("ignore", "integer argument expected", warnings.filterwarnings("ignore", "integer argument expected",
DeprecationWarning, "unittest") DeprecationWarning, "unittest")
# count the number of test runs.
# used to skip running test_execfile() multiple times
numruns = 0
class Squares: class Squares:
def __init__(self, max): def __init__(self, max):
@ -343,6 +348,11 @@ class BuiltinTest(unittest.TestCase):
execfile(TESTFN) execfile(TESTFN)
def test_execfile(self): def test_execfile(self):
global numruns
if numruns:
return
numruns += 1
globals = {'a': 1, 'b': 2} globals = {'a': 1, 'b': 2}
locals = {'b': 200, 'c': 300} locals = {'b': 200, 'c': 300}
@ -845,6 +855,30 @@ class BuiltinTest(unittest.TestCase):
self.assertEqual(max(1L, 2.0, 3), 3) self.assertEqual(max(1L, 2.0, 3), 3)
self.assertEqual(max(1.0, 2, 3L), 3L) self.assertEqual(max(1.0, 2, 3L), 3L)
for stmt in (
"max(key=int)", # no args
"max(1, key=int)", # single arg not iterable
"max(1, 2, keystone=int)", # wrong keyword
"max(1, 2, key=int, abc=int)", # two many keywords
"max(1, 2, key=1)", # keyfunc is not callable
):
try:
exec(stmt) in globals()
except TypeError:
pass
else:
self.fail(stmt)
self.assertEqual(max((1,), key=neg), 1) # one elem iterable
self.assertEqual(max((1,2), key=neg), 1) # two elem iterable
self.assertEqual(max(1, 2, key=neg), 1) # two elems
data = [random.randrange(200) for i in range(100)]
keys = dict((elem, random.randrange(50)) for elem in data)
f = keys.__getitem__
self.assertEqual(max(data, key=f),
sorted(reversed(data), key=f)[-1])
def test_min(self): def test_min(self):
self.assertEqual(min('123123'), '1') self.assertEqual(min('123123'), '1')
self.assertEqual(min(1, 2, 3), 1) self.assertEqual(min(1, 2, 3), 1)
@ -867,6 +901,30 @@ class BuiltinTest(unittest.TestCase):
raise ValueError raise ValueError
self.assertRaises(ValueError, min, (42, BadNumber())) self.assertRaises(ValueError, min, (42, BadNumber()))
for stmt in (
"min(key=int)", # no args
"min(1, key=int)", # single arg not iterable
"min(1, 2, keystone=int)", # wrong keyword
"min(1, 2, key=int, abc=int)", # two many keywords
"min(1, 2, key=1)", # keyfunc is not callable
):
try:
exec(stmt) in globals()
except TypeError:
pass
else:
self.fail(stmt)
self.assertEqual(min((1,), key=neg), 1) # one elem iterable
self.assertEqual(min((1,2), key=neg), 2) # two elem iterable
self.assertEqual(min(1, 2, key=neg), 2) # two elems
data = [random.randrange(200) for i in range(100)]
keys = dict((elem, random.randrange(50)) for elem in data)
f = keys.__getitem__
self.assertEqual(min(data, key=f),
sorted(data, key=f)[0])
def test_oct(self): def test_oct(self):
self.assertEqual(oct(100), '0144') self.assertEqual(oct(100), '0144')
self.assertEqual(oct(100L), '0144L') self.assertEqual(oct(100L), '0144L')
@ -1313,8 +1371,21 @@ class TestSorted(unittest.TestCase):
data = 'The quick Brown fox Jumped over The lazy Dog'.split() data = 'The quick Brown fox Jumped over The lazy Dog'.split()
self.assertRaises(TypeError, sorted, data, None, lambda x,y: 0) self.assertRaises(TypeError, sorted, data, None, lambda x,y: 0)
def test_main(): def test_main(verbose=None):
test.test_support.run_unittest(BuiltinTest, TestSorted) test_classes = (BuiltinTest, TestSorted)
run_unittest(*test_classes)
# verify reference counting
if verbose and hasattr(sys, "gettotalrefcount"):
import gc
counts = [None] * 5
for i in xrange(len(counts)):
run_unittest(*test_classes)
gc.collect()
counts[i] = sys.gettotalrefcount()
print counts
if __name__ == "__main__": if __name__ == "__main__":
test_main() test_main(verbose=True)

View file

@ -52,6 +52,7 @@ Alexander Belopolsky
Andy Bensky Andy Bensky
Michel Van den Bergh Michel Van den Bergh
Eric Beser Eric Beser
Steven Bethard
Stephen Bevan Stephen Bevan
Ron Bickers Ron Bickers
Dominic Binks Dominic Binks

View file

@ -10,6 +10,9 @@ What's New in Python 2.5 alpha 1?
Core and builtins Core and builtins
----------------- -----------------
- min() and max() now support key= arguments with the same meaning as in
list.sort().
Extension Modules Extension Modules
----------------- -----------------
@ -19,7 +22,7 @@ Library
------- -------
- heapq.nsmallest() and heapq.nlargest() now support key= arguments with - heapq.nsmallest() and heapq.nlargest() now support key= arguments with
the same meaning as for list.sort(). the same meaning as in list.sort().
Build Build

View file

@ -1114,82 +1114,114 @@ Update and return a dictionary containing the current scope's local variables.")
static PyObject * static PyObject *
min_max(PyObject *args, int op) min_max(PyObject *args, PyObject *kwds, int op)
{ {
PyObject *v, *it, *item, *val, *maxitem, *maxval, *keyfunc=NULL;
const char *name = op == Py_LT ? "min" : "max"; const char *name = op == Py_LT ? "min" : "max";
PyObject *v, *w, *x, *it;
if (PyTuple_Size(args) > 1) if (PyTuple_Size(args) > 1)
v = args; v = args;
else if (!PyArg_UnpackTuple(args, (char *)name, 1, 1, &v)) else if (!PyArg_UnpackTuple(args, (char *)name, 1, 1, &v))
return NULL; return NULL;
if (kwds != NULL && PyDict_Check(kwds) && PyDict_Size(kwds)) {
keyfunc = PyDict_GetItemString(kwds, "key");
if (PyDict_Size(kwds)!=1 || keyfunc == NULL) {
PyErr_Format(PyExc_TypeError,
"%s() got an unexpected keyword argument", name);
return NULL;
}
}
it = PyObject_GetIter(v); it = PyObject_GetIter(v);
if (it == NULL) if (it == NULL)
return NULL; return NULL;
w = NULL; /* the result */ maxitem = NULL; /* the result */
for (;;) { maxval = NULL; /* the value associated with the result */
x = PyIter_Next(it); while (item = PyIter_Next(it)) {
if (x == NULL) { /* get the value from the key function */
if (PyErr_Occurred()) { if (keyfunc != NULL) {
Py_XDECREF(w); val = PyObject_CallFunctionObjArgs(keyfunc, item, NULL);
Py_DECREF(it); if (val == NULL)
return NULL; goto Fail_it_item;
} }
break; /* no key function; the value is the item */
else {
val = item;
Py_INCREF(val);
} }
if (w == NULL) /* maximum value and item are unset; set them */
w = x; if (maxval == NULL) {
maxitem = item;
maxval = val;
}
/* maximum value and item are set; update them as necessary */
else { else {
int cmp = PyObject_RichCompareBool(x, w, op); int cmp = PyObject_RichCompareBool(val, maxval, op);
if (cmp > 0) { if (cmp < 0)
Py_DECREF(w); goto Fail_it_item_and_val;
w = x; else if (cmp > 0) {
Py_DECREF(maxval);
Py_DECREF(maxitem);
maxval = val;
maxitem = item;
} }
else if (cmp < 0) { else {
Py_DECREF(x); Py_DECREF(item);
Py_DECREF(w); Py_DECREF(val);
Py_DECREF(it);
return NULL;
} }
else
Py_DECREF(x);
} }
} }
if (w == NULL) if (PyErr_Occurred())
goto Fail_it;
if (maxval == NULL) {
PyErr_Format(PyExc_ValueError, PyErr_Format(PyExc_ValueError,
"%s() arg is an empty sequence", name); "%s() arg is an empty sequence", name);
assert(maxitem == NULL);
}
else
Py_DECREF(maxval);
Py_DECREF(it); Py_DECREF(it);
return w; return maxitem;
Fail_it_item_and_val:
Py_DECREF(val);
Fail_it_item:
Py_DECREF(item);
Fail_it:
Py_XDECREF(maxval);
Py_XDECREF(maxitem);
Py_DECREF(it);
return NULL;
} }
static PyObject * static PyObject *
builtin_min(PyObject *self, PyObject *v) builtin_min(PyObject *self, PyObject *args, PyObject *kwds)
{ {
return min_max(v, Py_LT); return min_max(args, kwds, Py_LT);
} }
PyDoc_STRVAR(min_doc, PyDoc_STRVAR(min_doc,
"min(sequence) -> value\n\ "min(iterable[, key=func]) -> value\n\
min(a, b, c, ...) -> value\n\ min(a, b, c, ...[, key=func]) -> value\n\
\n\ \n\
With a single sequence argument, return its smallest item.\n\ With a single iterable argument, return its smallest item.\n\
With two or more arguments, return the smallest argument."); With two or more arguments, return the smallest argument.");
static PyObject * static PyObject *
builtin_max(PyObject *self, PyObject *v) builtin_max(PyObject *self, PyObject *args, PyObject *kwds)
{ {
return min_max(v, Py_GT); return min_max(args, kwds, Py_GT);
} }
PyDoc_STRVAR(max_doc, PyDoc_STRVAR(max_doc,
"max(sequence) -> value\n\ "max(iterable[, key=func]) -> value\n\
max(a, b, c, ...) -> value\n\ max(a, b, c, ...[, key=func]) -> value\n\
\n\ \n\
With a single sequence argument, return its largest item.\n\ With a single iterable argument, return its largest item.\n\
With two or more arguments, return the largest argument."); With two or more arguments, return the largest argument.");
@ -2119,8 +2151,8 @@ static PyMethodDef builtin_methods[] = {
{"len", builtin_len, METH_O, len_doc}, {"len", builtin_len, METH_O, len_doc},
{"locals", (PyCFunction)builtin_locals, METH_NOARGS, locals_doc}, {"locals", (PyCFunction)builtin_locals, METH_NOARGS, locals_doc},
{"map", builtin_map, METH_VARARGS, map_doc}, {"map", builtin_map, METH_VARARGS, map_doc},
{"max", builtin_max, METH_VARARGS, max_doc}, {"max", (PyCFunction)builtin_max, METH_VARARGS | METH_KEYWORDS, max_doc},
{"min", builtin_min, METH_VARARGS, min_doc}, {"min", (PyCFunction)builtin_min, METH_VARARGS | METH_KEYWORDS, min_doc},
{"oct", builtin_oct, METH_O, oct_doc}, {"oct", builtin_oct, METH_O, oct_doc},
{"ord", builtin_ord, METH_O, ord_doc}, {"ord", builtin_ord, METH_O, ord_doc},
{"pow", builtin_pow, METH_VARARGS, pow_doc}, {"pow", builtin_pow, METH_VARARGS, pow_doc},