]> granicus.if.org Git - python/commitdiff
SF patch #1077353: add key= argument to min and max
authorRaymond Hettinger <python@rcn.com>
Fri, 3 Dec 2004 08:30:39 +0000 (08:30 +0000)
committerRaymond Hettinger <python@rcn.com>
Fri, 3 Dec 2004 08:30:39 +0000 (08:30 +0000)
(First draft of patch contributed by Steven Bethard.)

Doc/lib/libfuncs.tex
Lib/test/test_builtin.py
Misc/ACKS
Misc/NEWS
Python/bltinmodule.c

index 2099f3f52bbf203085c4d2b243e921f4d6fc377b..904b4e1d3e373873ad473fb3accb9e4c8575e780 100644 (file)
@@ -642,16 +642,28 @@ class C:
   of sequence; the result is always a list.
 \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
   non-empty sequence (such as a string, tuple or list).  With more
   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}
 
 \begin{funcdesc}{min}{s\optional{, args...}}
   With a single argument \var{s}, return the smallest item of a
   non-empty sequence (such as a string, tuple or list).  With more
   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}
 
 \begin{funcdesc}{object}{}
index f023b117dc8e23bcc9c64b587b8930470a3adf0c..1dae60339778981a080a90dda1a632059e362f78 100644 (file)
@@ -1,7 +1,8 @@
 # Python test set -- built-in functions
 
 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
 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",
                         DeprecationWarning, "unittest")
 
+# count the number of test runs.
+# used to skip running test_execfile() multiple times
+numruns = 0
+
 class Squares:
 
     def __init__(self, max):
@@ -343,6 +348,11 @@ class BuiltinTest(unittest.TestCase):
     execfile(TESTFN)
 
     def test_execfile(self):
+        global numruns
+        if numruns:
+            return
+        numruns += 1
+        
         globals = {'a': 1, 'b': 2}
         locals = {'b': 200, 'c': 300}
 
@@ -845,6 +855,30 @@ class BuiltinTest(unittest.TestCase):
         self.assertEqual(max(1L, 2.0, 3), 3)
         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):
         self.assertEqual(min('123123'), '1')
         self.assertEqual(min(1, 2, 3), 1)
@@ -867,6 +901,30 @@ class BuiltinTest(unittest.TestCase):
                 raise ValueError
         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):
         self.assertEqual(oct(100), '0144')
         self.assertEqual(oct(100L), '0144L')
@@ -1313,8 +1371,21 @@ class TestSorted(unittest.TestCase):
         data = 'The quick Brown fox Jumped over The lazy Dog'.split()
         self.assertRaises(TypeError, sorted, data, None, lambda x,y: 0)
 
-def test_main():
-    test.test_support.run_unittest(BuiltinTest, TestSorted)
+def test_main(verbose=None):
+    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__":
-    test_main()
+    test_main(verbose=True)
index 2c8038fc7b38caced322f6a372562f08745ad55c..c83d479642501efabde8376319d8ad8e355e8e0d 100644 (file)
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -52,6 +52,7 @@ Alexander Belopolsky
 Andy Bensky
 Michel Van den Bergh
 Eric Beser
+Steven Bethard
 Stephen Bevan
 Ron Bickers
 Dominic Binks
index 4029c1de43a2f4b0573d0205b4f23ca376c6e21a..a1dfa457ceeb9c2ac33850185e796f176201d7ee 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -10,6 +10,9 @@ What's New in Python 2.5 alpha 1?
 Core and builtins
 -----------------
 
+- min() and max() now support key= arguments with the same meaning as in
+  list.sort().
+
 
 Extension Modules
 -----------------
@@ -19,7 +22,7 @@ Library
 -------
 
 - 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
index 6fbe799f71139e9b4b27040e5488edec1f949ed0..e189952b49dbee053eb8617acf4598ea72bc0119 100644 (file)
@@ -1114,82 +1114,114 @@ Update and return a dictionary containing the current scope's local variables.")
 
 
 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";
-       PyObject *v, *w, *x, *it;
 
        if (PyTuple_Size(args) > 1)
                v = args;
        else if (!PyArg_UnpackTuple(args, (char *)name, 1, 1, &v))
                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);
        if (it == NULL)
                return NULL;
 
-       w = NULL;  /* the result */
-       for (;;) {
-               x = PyIter_Next(it);
-               if (x == NULL) {
-                       if (PyErr_Occurred()) {
-                               Py_XDECREF(w);
-                               Py_DECREF(it);
-                               return NULL;
-                       }
-                       break;
+       maxitem = NULL; /* the result */
+       maxval = NULL;  /* the value associated with the result */
+       while (item = PyIter_Next(it)) {
+               /* get the value from the key function */
+               if (keyfunc != NULL) {
+                       val = PyObject_CallFunctionObjArgs(keyfunc, item, NULL);
+                       if (val == NULL)
+                               goto Fail_it_item;
+               }
+               /* no key function; the value is the item */
+               else {
+                       val = item;
+                       Py_INCREF(val);
                }
 
-               if (w == NULL)
-                       w = x;
+               /* maximum value and item are unset; set them */
+               if (maxval == NULL) {
+                       maxitem = item;
+                       maxval = val;
+               }
+               /* maximum value and item are set; update them as necessary */
                else {
-                       int cmp = PyObject_RichCompareBool(x, w, op);
-                       if (cmp > 0) {
-                               Py_DECREF(w);
-                               w = x;
+                       int cmp = PyObject_RichCompareBool(val, maxval, op);
+                       if (cmp < 0)
+                               goto Fail_it_item_and_val;
+                       else if (cmp > 0) {
+                               Py_DECREF(maxval);
+                               Py_DECREF(maxitem);
+                               maxval = val;
+                               maxitem = item;
                        }
-                       else if (cmp < 0) {
-                               Py_DECREF(x);
-                               Py_DECREF(w);
-                               Py_DECREF(it);
-                               return NULL;
+                       else {
+                               Py_DECREF(item);
+                               Py_DECREF(val);
                        }
-                       else
-                               Py_DECREF(x);
                }
        }
-       if (w == NULL)
+       if (PyErr_Occurred())
+               goto Fail_it;
+       if (maxval == NULL) {
                PyErr_Format(PyExc_ValueError,
                             "%s() arg is an empty sequence", name);
+               assert(maxitem == NULL);
+       }
+       else
+               Py_DECREF(maxval);
        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 *
-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,
-"min(sequence) -> value\n\
-min(a, b, c, ...) -> value\n\
+"min(iterable[, key=func]) -> value\n\
+min(a, b, c, ...[, key=func]) -> value\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.");
 
 
 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,
-"max(sequence) -> value\n\
-max(a, b, c, ...) -> value\n\
+"max(iterable[, key=func]) -> value\n\
+max(a, b, c, ...[, key=func]) -> value\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.");
 
 
@@ -2119,8 +2151,8 @@ static PyMethodDef builtin_methods[] = {
        {"len",         builtin_len,        METH_O, len_doc},
        {"locals",      (PyCFunction)builtin_locals,     METH_NOARGS, locals_doc},
        {"map",         builtin_map,        METH_VARARGS, map_doc},
-       {"max",         builtin_max,        METH_VARARGS, max_doc},
-       {"min",         builtin_min,        METH_VARARGS, min_doc},
+       {"max",         (PyCFunction)builtin_max,        METH_VARARGS | METH_KEYWORDS, max_doc},
+       {"min",         (PyCFunction)builtin_min,        METH_VARARGS | METH_KEYWORDS, min_doc},
        {"oct",         builtin_oct,        METH_O, oct_doc},
        {"ord",         builtin_ord,        METH_O, ord_doc},
        {"pow",         builtin_pow,        METH_VARARGS, pow_doc},