summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRaymond Hettinger <python@rcn.com>2005-02-28 19:39:44 (GMT)
committerRaymond Hettinger <python@rcn.com>2005-02-28 19:39:44 (GMT)
commit9c323f8de4910dfc0baa5e55aa84eb1b02bcbb72 (patch)
tree7f0eecf7973ff979710eb1cb1795598e55736774
parent049ade2997aee8cd6564e05d29dbe0b390ebf27b (diff)
downloadcpython-9c323f8de4910dfc0baa5e55aa84eb1b02bcbb72.zip
cpython-9c323f8de4910dfc0baa5e55aa84eb1b02bcbb72.tar.gz
cpython-9c323f8de4910dfc0baa5e55aa84eb1b02bcbb72.tar.bz2
SF patch #941881: PEP 309 Implementation (Partial Function Application).
Combined efforts of many including Peter Harris, Hye-Shik Chang, Martin v. Löwis, Nick Coghlan, Paul Moore, and Raymond Hettinger.
-rw-r--r--Doc/lib/lib.tex1
-rw-r--r--Doc/lib/libfunctional.tex72
-rw-r--r--Lib/test/test_functional.py154
-rw-r--r--Misc/NEWS2
-rw-r--r--Modules/functionalmodule.c225
-rw-r--r--PC/VC6/pythoncore.dsp4
-rw-r--r--PC/config.c2
-rw-r--r--PCbuild/pythoncore.vcproj27
-rw-r--r--setup.py2
9 files changed, 489 insertions, 0 deletions
diff --git a/Doc/lib/lib.tex b/Doc/lib/lib.tex
index 37ab91d..9913449 100644
--- a/Doc/lib/lib.tex
+++ b/Doc/lib/lib.tex
@@ -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
index 0000000..c092d6d
--- /dev/null
+++ b/Doc/lib/libfunctional.tex
@@ -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
index 0000000..db3a289
--- /dev/null
+++ b/Lib/test/test_functional.py
@@ -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)
diff --git a/Misc/NEWS b/Misc/NEWS
index 68762a6..4add264 100644
--- 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
index 0000000..18efab2
--- /dev/null
+++ b/Modules/functionalmodule.c
@@ -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]);
+ }
+}
diff --git a/PC/VC6/pythoncore.dsp b/PC/VC6/pythoncore.dsp
index 357ad49..8fbb998 100644
--- a/PC/VC6/pythoncore.dsp
+++ b/PC/VC6/pythoncore.dsp
@@ -301,6 +301,10 @@ SOURCE=..\..\Objects\funcobject.c
# End Source File
# Begin Source File
+SOURCE=..\..\Modules\functionalmodule.c
+# End Source File
+# Begin Source File
+
SOURCE=..\..\Python\future.c
# End Source File
# Begin Source File
diff --git a/PC/config.c b/PC/config.c
index 983255a..bd040b0 100644
--- a/PC/config.c
+++ b/PC/config.c
@@ -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},
diff --git a/PCbuild/pythoncore.vcproj b/PCbuild/pythoncore.vcproj
index 1bf6704..3d8aaf2 100644
--- a/PCbuild/pythoncore.vcproj
+++ b/PCbuild/pythoncore.vcproj
@@ -1294,6 +1294,33 @@
</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
Name="Release|Win32">
diff --git a/setup.py b/setup.py
index 9b41f4c..85322a8 100644
--- 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