From 9c323f8de4910dfc0baa5e55aa84eb1b02bcbb72 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Mon, 28 Feb 2005 19:39:44 +0000 Subject: SF patch #941881: PEP 309 Implementation (Partial Function Application). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 | 1 + Doc/lib/libfunctional.tex | 72 ++++++++++++++ Lib/test/test_functional.py | 154 ++++++++++++++++++++++++++++++ Misc/NEWS | 2 + Modules/functionalmodule.c | 225 ++++++++++++++++++++++++++++++++++++++++++++ PC/VC6/pythoncore.dsp | 4 + PC/config.c | 2 + PCbuild/pythoncore.vcproj | 27 ++++++ setup.py | 2 + 9 files changed, 489 insertions(+) create mode 100644 Doc/lib/libfunctional.tex create mode 100644 Lib/test/test_functional.py create mode 100644 Modules/functionalmodule.c 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 + with adaptations by Raymond Hettinger + 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 @@ + + + + + + + + + + + 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 -- cgit v0.12