]> granicus.if.org Git - python/commitdiff
SF patch #941881: PEP 309 Implementation (Partial Function Application).
authorRaymond Hettinger <python@rcn.com>
Mon, 28 Feb 2005 19:39:44 +0000 (19:39 +0000)
committerRaymond Hettinger <python@rcn.com>
Mon, 28 Feb 2005 19:39:44 +0000 (19:39 +0000)
Combined efforts of many including Peter Harris, Hye-Shik Chang,
Martin v. Löwis, Nick Coghlan, Paul Moore, and Raymond Hettinger.

Doc/lib/lib.tex
Doc/lib/libfunctional.tex [new file with mode: 0644]
Lib/test/test_functional.py [new file with mode: 0644]
Misc/NEWS
Modules/functionalmodule.c [new file with mode: 0644]
PC/VC6/pythoncore.dsp
PC/config.c
PCbuild/pythoncore.vcproj
setup.py

index 37ab91d0af8c52e78bc9b7c7474ee04eaead8c12..9913449bf42e4dca2b2ff29fb37f3b08843da7a2 100644 (file)
@@ -132,6 +132,7 @@ and how to embed it in other applications.
 \input{libarray}
 \input{libsets}
 \input{libitertools}
+\input{libfunctional}
 \input{libcfgparser}
 \input{libfileinput}
 \input{libcalendar}
diff --git a/Doc/lib/libfunctional.tex b/Doc/lib/libfunctional.tex
new file mode 100644 (file)
index 0000000..c092d6d
--- /dev/null
@@ -0,0 +1,72 @@
+\section{\module{functional} ---
+         Higher order functions and operations on callable objects.}
+
+\declaremodule{standard}{functional}           % standard library, in Python
+
+\moduleauthor{Peter Harris}{scav@blueyonder.co.uk}
+\moduleauthor{Raymond Hettinger}{python@rcn.com}
+\sectionauthor{Peter Harris}{scav@blueyonder.co.uk}
+
+\modulesynopsis{Higher-order functions and operations on callable objects.}
+
+
+The \module{functional} module is for higher-order functions: functions
+that act on or return other functions. In general, any callable object can
+be treated as a function for the purposes of this module.
+
+
+The \module{functional} module defines the following function:
+
+\begin{funcdesc}{partial}{func\optional{,*args}\optional{, **keywords}}
+Return a new \class{partial} object which when called will behave like
+\var{func} called with the positional arguments \var{args} and keyword
+arguments \var{keywords}. If more arguments are supplied to the call, they
+are appended to \var{args}. If additional keyword arguments are supplied,
+they extend and override \var{keywords}. Roughly equivalent to:
+  \begin{verbatim}
+        def partial(func, *args, **keywords):
+            def newfunc(*fargs, **fkeywords):
+                newkeywords = keywords.copy()
+                newkeywords.update(fkeywords)
+                return func(*(args + fargs), **newkeywords)
+            newfunc.func = func
+            newfunc.args = args
+            newfunc.keywords = keywords
+            return newfunc
+  \end{verbatim}
+
+The \function{partial} is used for partial function application which
+``freezes'' some portion of a function's arguments and/or keywords
+resulting in an new object with a simplified signature.  For example,
+\function{partial} can be used to create a callable that behaves like
+the \function{int} function where the \var{base} argument defaults to
+two:
+  \begin{verbatim}
+        >>> basetwo = partial(int, base=2)
+        >>> basetwo('10010')
+        18
+  \end{verbatim}
+\end{funcdesc}
+
+
+
+\subsection{\class{partial} Objects \label{partial-objects}}
+
+
+\class{partial} objects are callable objects created by \function{partial()}.
+They have three read-only attributes:
+
+\begin{memberdesc}[callable]{func}{}
+A callable object or function.  Calls to the \class{partial} object will
+be forwarded to \member{func} with new arguments and keywords.
+\end{memberdesc}
+
+\begin{memberdesc}[tuple]{args}{}
+The leftmost positional arguments that will be prepended to the
+positional arguments provided to a \class{partial} object call.
+\end{memberdesc}
+
+\begin{memberdesc}[dict]{keywords}{}
+The keyword arguments that will be supplied when the \class{partial} object
+is called.
+\end{memberdesc}
diff --git a/Lib/test/test_functional.py b/Lib/test/test_functional.py
new file mode 100644 (file)
index 0000000..db3a289
--- /dev/null
@@ -0,0 +1,154 @@
+import functional
+import unittest
+from test import test_support
+
+@staticmethod
+def PythonPartial(func, *args, **keywords):
+    'Pure Python approximation of partial()'
+    def newfunc(*fargs, **fkeywords):
+        newkeywords = keywords.copy()
+        newkeywords.update(fkeywords)
+        return func(*(args + fargs), **newkeywords)
+    newfunc.func = func
+    newfunc.args = args
+    newfunc.keywords = keywords
+    return newfunc
+
+def capture(*args, **kw):
+    """capture all positional and keyword arguments"""
+    return args, kw
+
+class TestPartial(unittest.TestCase):
+
+    thetype = functional.partial
+
+    def test_basic_examples(self):
+        p = self.thetype(capture, 1, 2, a=10, b=20)
+        self.assertEqual(p(3, 4, b=30, c=40),
+                         ((1, 2, 3, 4), dict(a=10, b=30, c=40)))
+        p = self.thetype(map, lambda x: x*10)
+        self.assertEqual(p([1,2,3,4]), [10, 20, 30, 40])
+
+    def test_attributes(self):
+        p = self.thetype(capture, 1, 2, a=10, b=20)
+        # attributes should be readable
+        self.assertEqual(p.func, capture)
+        self.assertEqual(p.args, (1, 2))
+        self.assertEqual(p.keywords, dict(a=10, b=20))
+        # attributes should not be writable
+        if not isinstance(self.thetype, type):
+            return
+        self.assertRaises(TypeError, setattr, p, 'func', map)
+        self.assertRaises(TypeError, setattr, p, 'args', (1, 2))
+        self.assertRaises(TypeError, setattr, p, 'keywords', dict(a=1, b=2))
+
+    def test_argument_checking(self):
+        self.assertRaises(TypeError, self.thetype)     # need at least a func arg
+        try:
+            self.thetype(2)()
+        except TypeError:
+            pass
+        else:
+            self.fail('First arg not checked for callability')
+
+    def test_protection_of_callers_dict_argument(self):
+        # a caller's dictionary should not be altered by partial
+        def func(a=10, b=20):
+            return a
+        d = {'a':3}
+        p = self.thetype(func, a=5)
+        self.assertEqual(p(**d), 3)
+        self.assertEqual(d, {'a':3})
+        p(b=7)
+        self.assertEqual(d, {'a':3})
+
+    def test_arg_combinations(self):
+        # exercise special code paths for zero args in either partial
+        # object or the caller
+        p = self.thetype(capture)
+        self.assertEqual(p(), ((), {}))
+        self.assertEqual(p(1,2), ((1,2), {}))
+        p = self.thetype(capture, 1, 2)
+        self.assertEqual(p(), ((1,2), {}))
+        self.assertEqual(p(3,4), ((1,2,3,4), {}))
+
+    def test_kw_combinations(self):
+        # exercise special code paths for no keyword args in
+        # either the partial object or the caller
+        p = self.thetype(capture)
+        self.assertEqual(p(), ((), {}))
+        self.assertEqual(p(a=1), ((), {'a':1}))
+        p = self.thetype(capture, a=1)
+        self.assertEqual(p(), ((), {'a':1}))
+        self.assertEqual(p(b=2), ((), {'a':1, 'b':2}))
+        # keyword args in the call override those in the partial object
+        self.assertEqual(p(a=3, b=2), ((), {'a':3, 'b':2}))
+
+    def test_positional(self):
+        # make sure positional arguments are captured correctly
+        for args in [(), (0,), (0,1), (0,1,2), (0,1,2,3)]:
+            p = self.thetype(capture, *args)
+            expected = args + ('x',)
+            got, empty = p('x')
+            self.failUnless(expected == got and empty == {})
+
+    def test_keyword(self):
+        # make sure keyword arguments are captured correctly
+        for a in ['a', 0, None, 3.5]:
+            p = self.thetype(capture, a=a)
+            expected = {'a':a,'x':None}
+            empty, got = p(x=None)
+            self.failUnless(expected == got and empty == ())
+
+    def test_no_side_effects(self):
+        # make sure there are no side effects that affect subsequent calls
+        p = self.thetype(capture, 0, a=1)
+        args1, kw1 = p(1, b=2)
+        self.failUnless(args1 == (0,1) and kw1 == {'a':1,'b':2})
+        args2, kw2 = p()
+        self.failUnless(args2 == (0,) and kw2 == {'a':1})
+
+    def test_error_propagation(self):
+        def f(x, y):
+            x / y
+        self.assertRaises(ZeroDivisionError, self.thetype(f, 1, 0))
+        self.assertRaises(ZeroDivisionError, self.thetype(f, 1), 0)
+        self.assertRaises(ZeroDivisionError, self.thetype(f), 1, 0)
+        self.assertRaises(ZeroDivisionError, self.thetype(f, y=0), 1)
+
+
+class PartialSubclass(functional.partial):
+    pass
+
+class TestPartialSubclass(TestPartial):
+
+    thetype = PartialSubclass
+
+
+class TestPythonPartial(TestPartial):
+
+    thetype = PythonPartial
+
+
+
+def test_main(verbose=None):
+    import sys
+    test_classes = (
+        TestPartial,
+        TestPartialSubclass,
+        TestPythonPartial,
+    )
+    test_support.run_unittest(*test_classes)
+
+    # verify reference counting
+    if verbose and hasattr(sys, "gettotalrefcount"):
+        import gc
+        counts = [None] * 5
+        for i in xrange(len(counts)):
+            test_support.run_unittest(*test_classes)
+            gc.collect()
+            counts[i] = sys.gettotalrefcount()
+        print counts
+
+if __name__ == '__main__':
+    test_main(verbose=True)
index 68762a6f27fab19ce78d88b5e80c3d713faf00b3..4add264fb9c3563a89435d84d43b7d071b1048ae 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -34,6 +34,8 @@ Core and builtins
 Extension Modules
 -----------------
 
+- Added functional.partial().  See PEP309.
+
 - Patch #1093585: raise a ValueError for negative history items in readline.
   {remove_history,replace_history}
 
diff --git a/Modules/functionalmodule.c b/Modules/functionalmodule.c
new file mode 100644 (file)
index 0000000..18efab2
--- /dev/null
@@ -0,0 +1,225 @@
+
+#include "Python.h"
+#include "structmember.h"
+
+/* Functional module written and maintained 
+   by Hye-Shik Chang <perky@FreeBSD.org>
+   with adaptations by Raymond Hettinger <python@rcn.com>
+   Copyright (c) 2004, 2005 Python Software Foundation.
+   All rights reserved.
+*/
+
+/* partial object **********************************************************/
+
+typedef struct {
+       PyObject_HEAD
+       PyObject *fn;
+       PyObject *args;
+       PyObject *kw;
+} partialobject;
+
+static PyTypeObject partial_type;
+
+static PyObject *
+partial_new(PyTypeObject *type, PyObject *args, PyObject *kw)
+{
+       PyObject *func;
+       partialobject *pto;
+
+       if (PyTuple_GET_SIZE(args) < 1) {
+               PyErr_SetString(PyExc_TypeError,
+                               "type 'partial' takes at least one argument");
+               return NULL;
+       }
+
+       func = PyTuple_GET_ITEM(args, 0);
+       if (!PyCallable_Check(func)) {
+               PyErr_SetString(PyExc_TypeError,
+                               "the first argument must be callable");
+               return NULL;
+       }
+
+       /* create partialobject structure */
+       pto = (partialobject *)type->tp_alloc(type, 0);
+       if (pto == NULL)
+               return NULL;
+
+       pto->fn = func;
+       Py_INCREF(func);
+       pto->args = PyTuple_GetSlice(args, 1, INT_MAX);
+       if (pto->args == NULL) {
+               pto->kw = NULL;
+               Py_DECREF(pto);
+               return NULL;
+       }
+       if (kw != NULL) {
+               pto->kw = PyDict_Copy(kw);
+               if (pto->kw == NULL) {
+                       Py_DECREF(pto);
+                       return NULL;
+               }
+       } else {
+               pto->kw = Py_None;
+               Py_INCREF(Py_None);
+       }
+
+       return (PyObject *)pto;
+}
+
+static void
+partial_dealloc(partialobject *pto)
+{
+       PyObject_GC_UnTrack(pto);
+       Py_XDECREF(pto->fn);
+       Py_XDECREF(pto->args);
+       Py_XDECREF(pto->kw);
+       pto->ob_type->tp_free(pto);
+}
+
+static PyObject *
+partial_call(partialobject *pto, PyObject *args, PyObject *kw)
+{
+       PyObject *ret;
+       PyObject *argappl = NULL, *kwappl = NULL;
+
+       assert (PyCallable_Check(pto->fn));
+       assert (PyTuple_Check(pto->args));
+       assert (pto->kw == Py_None  ||  PyDict_Check(pto->kw));
+
+       if (PyTuple_GET_SIZE(pto->args) == 0) {
+               argappl = args;
+               Py_INCREF(args);
+       } else if (PyTuple_GET_SIZE(args) == 0) {
+               argappl = pto->args;
+               Py_INCREF(pto->args);
+       } else {
+               argappl = PySequence_Concat(pto->args, args);
+               if (argappl == NULL)
+                       return NULL;
+       }
+
+       if (pto->kw == Py_None) {
+               kwappl = kw;
+               Py_XINCREF(kw);
+       } else {
+               kwappl = PyDict_Copy(pto->kw);
+               if (kwappl == NULL) {
+                       Py_DECREF(argappl);
+                       return NULL;
+               }
+               if (kw != NULL) {
+                       if (PyDict_Merge(kwappl, kw, 1) != 0) {
+                               Py_DECREF(argappl);
+                               Py_DECREF(kwappl);
+                               return NULL;
+                       }
+               }
+       }
+
+       ret = PyObject_Call(pto->fn, argappl, kwappl);
+       Py_DECREF(argappl);
+       Py_XDECREF(kwappl);
+       return ret;
+}
+
+static int
+partial_traverse(partialobject *pto, visitproc visit, void *arg)
+{
+       Py_VISIT(pto->fn);
+       Py_VISIT(pto->args);
+       Py_VISIT(pto->kw);
+       return 0;
+}
+
+PyDoc_STRVAR(partial_doc,
+"partial(func, *args, **keywords) - new function with partial application\n\
+       of the given arguments and keywords.\n");
+
+#define OFF(x) offsetof(partialobject, x)
+static PyMemberDef partial_memberlist[] = {
+       {"func",        T_OBJECT,       OFF(fn),        READONLY,
+        "function object to use in future partial calls"},
+       {"args",        T_OBJECT,       OFF(args),      READONLY,
+        "tuple of arguments to future partial calls"},
+       {"keywords",    T_OBJECT,       OFF(kw),        READONLY,
+        "dictionary of keyword arguments to future partial calls"},
+       {NULL}  /* Sentinel */
+};
+
+static PyTypeObject partial_type = {
+       PyObject_HEAD_INIT(NULL)
+       0,                              /* ob_size */
+       "functional.partial",           /* tp_name */
+       sizeof(partialobject),          /* tp_basicsize */
+       0,                              /* tp_itemsize */
+       /* methods */
+       (destructor)partial_dealloc,    /* tp_dealloc */
+       0,                              /* tp_print */
+       0,                              /* tp_getattr */
+       0,                              /* tp_setattr */
+       0,                              /* tp_compare */
+       0,                              /* tp_repr */
+       0,                              /* tp_as_number */
+       0,                              /* tp_as_sequence */
+       0,                              /* tp_as_mapping */
+       0,                              /* tp_hash */
+       (ternaryfunc)partial_call,      /* tp_call */
+       0,                              /* tp_str */
+       PyObject_GenericGetAttr,        /* tp_getattro */
+       0,                              /* tp_setattro */
+       0,                              /* tp_as_buffer */
+       Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
+               Py_TPFLAGS_BASETYPE,    /* tp_flags */
+       partial_doc,                    /* tp_doc */
+       (traverseproc)partial_traverse, /* tp_traverse */
+       0,                              /* tp_clear */
+       0,                              /* tp_richcompare */
+       0,                              /* tp_weaklistoffset */
+       0,                              /* tp_iter */
+       0,                              /* tp_iternext */
+       0,                              /* tp_methods */
+       partial_memberlist,             /* tp_members */
+       0,                              /* tp_getset */
+       0,                              /* tp_base */
+       0,                              /* tp_dict */
+       0,                              /* tp_descr_get */
+       0,                              /* tp_descr_set */
+       0,                              /* tp_dictoffset */
+       0,                              /* tp_init */
+       0,                              /* tp_alloc */
+       partial_new,                    /* tp_new */
+       PyObject_GC_Del,                /* tp_free */
+};
+
+
+/* module level code ********************************************************/
+
+PyDoc_STRVAR(module_doc,
+"Tools for functional programming.");
+
+static PyMethodDef module_methods[] = {
+       {NULL,          NULL}           /* sentinel */
+};
+
+PyMODINIT_FUNC
+initfunctional(void)
+{
+       int i;
+       PyObject *m;
+       char *name;
+       PyTypeObject *typelist[] = {
+               &partial_type,
+               NULL
+       };
+
+       m = Py_InitModule3("functional", module_methods, module_doc);
+
+       for (i=0 ; typelist[i] != NULL ; i++) {
+               if (PyType_Ready(typelist[i]) < 0)
+                       return;
+               name = strchr(typelist[i]->tp_name, '.');
+               assert (name != NULL);
+               Py_INCREF(typelist[i]);
+               PyModule_AddObject(m, name+1, (PyObject *)typelist[i]);
+       }
+}
index 357ad49dd8a5ae1f4db684de82cc3f69f2a0a517..8fbb998f91f043425463c2b235f5b474d7736ff2 100644 (file)
@@ -301,6 +301,10 @@ SOURCE=..\..\Objects\funcobject.c
 # End Source File\r
 # Begin Source File\r
 \r
+SOURCE=..\..\Modules\functionalmodule.c\r
+# End Source File\r
+# Begin Source File\r
+\r
 SOURCE=..\..\Python\future.c\r
 # End Source File\r
 # Begin Source File\r
index 983255a58529d9511287b79c14999e20bb5fefea..bd040b08b8227fb48ab74d962f67ba55ab20cd48 100644 (file)
@@ -53,6 +53,7 @@ extern void init_sre(void);
 extern void initparser(void);
 extern void init_winreg(void);
 extern void initdatetime(void);
+extern void initfunctional(void);
 
 extern void init_multibytecodec(void);
 extern void init_codecs_cn(void);
@@ -124,6 +125,7 @@ struct _inittab _PyImport_Inittab[] = {
        {"parser", initparser},
        {"_winreg", init_winreg},
        {"datetime", initdatetime},
+       {"functional", initfunctional},
 
        {"xxsubtype", initxxsubtype},
        {"zipimport", initzipimport},
index 1bf6704e76756cc3210e779c6d465a4cdf2ee5b8..3d8aaf24f0c3dd2d5bc7fd15130498c9a8ab385c 100644 (file)
                                        PreprocessorDefinitions="NDEBUG;WIN32;_WINDOWS;USE_DL_EXPORT;$(NoInherit)"/>
                        </FileConfiguration>
                </File>
+               <File
+                       RelativePath="..\Modules\functionalmodule.c">
+                       <FileConfiguration
+                               Name="Release|Win32">
+                               <Tool
+                                       Name="VCCLCompilerTool"
+                                       Optimization="2"
+                                       AdditionalIncludeDirectories=""
+                                       PreprocessorDefinitions="NDEBUG;WIN32;_WINDOWS;USE_DL_EXPORT;$(NoInherit)"/>
+                       </FileConfiguration>
+                       <FileConfiguration
+                               Name="Debug|Win32">
+                               <Tool
+                                       Name="VCCLCompilerTool"
+                                       Optimization="0"
+                                       AdditionalIncludeDirectories=""
+                                       PreprocessorDefinitions="_DEBUG;USE_DL_EXPORT;WIN32;_WINDOWS;$(NoInherit)"/>
+                       </FileConfiguration>
+                       <FileConfiguration
+                               Name="ReleaseItanium|Win32">
+                               <Tool
+                                       Name="VCCLCompilerTool"
+                                       Optimization="2"
+                                       AdditionalIncludeDirectories=""
+                                       PreprocessorDefinitions="NDEBUG;WIN32;_WINDOWS;USE_DL_EXPORT;$(NoInherit)"/>
+                       </FileConfiguration>
+               </File>
                <File
                        RelativePath="..\Python\future.c">
                        <FileConfiguration
index 9b41f4c20c2301c738423fe2ae2fc9fb30dff340..85322a853fb31f5ceaa20576c5de23dd9778aaa6 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -355,6 +355,8 @@ class PyBuildExt(build_ext):
         exts.append( Extension("_heapq", ["_heapqmodule.c"]) )
         # operator.add() and similar goodies
         exts.append( Extension('operator', ['operator.c']) )
+        # functional
+        exts.append( Extension("functional", ["functionalmodule.c"]) )
         # Python C API test module
         exts.append( Extension('_testcapi', ['_testcapimodule.c']) )
         # static Unicode character database