Index: Misc/ACKS =================================================================== --- Misc/ACKS (revision 88194) +++ Misc/ACKS (working copy) @@ -42,6 +42,7 @@ Alfonso Baciero Marcin Bachry Dwayne Bailey +Jason Baker Stig Bakken Greg Ball Luigi Ballabio Index: Misc/NEWS =================================================================== --- Misc/NEWS (revision 88194) +++ Misc/NEWS (working copy) @@ -50,9 +50,12 @@ that for incoming headers which makes this behaviour now consistent in both incoming and outgoing direction. +- Added several more functional programming tools to functools. + - Issue #9509: argparse now properly handles IOErrors raised by argparse.FileType. + What's New in Python 3.2 Release Candidate 1 ============================================ Index: Doc/library/functools.rst =================================================================== --- Doc/library/functools.rst (revision 88194) +++ Doc/library/functools.rst (working copy) @@ -134,6 +134,8 @@ .. versionadded:: 3.2 +Common Functional Programming Functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. function:: partial(func, *args, **keywords) @@ -177,7 +179,60 @@ a default when the sequence is empty. If *initializer* is not given and *sequence* contains only one item, the first item is returned. +.. function:: flip(func) + Return a function that is equivalent to *func* except that it has its first two + arguments reversed. For instance ``flip(my_function)(a, b)`` is equivalent to + ``my_function(b, a)``. + +.. function:: const(retval) + + Return a function that will always return *retval*. + +.. function:: compose(func1, *funcs) + + Create a function that composes *func1* and *funcs* from left to right. + For example:: + + >>> from functools import compose, partial + >>> rejoin = compose(str.split, partial(str.join, '-')) + >>> rejoin("This is a string") + 'This-is-a-string' + +.. function:: identity(*args) + + If a single argument is given, return it. Otherwise, return a tuple of the + arguments given. Example:: + + >>> identity(1) + 1 + >>> identity(1, 2, 3) + (1, 2, 3) + >>> identity() + () + +.. function:: trampoline(func, *args, **kwargs) + + Calls func with *args* and *kwargs*. If the result is a callable, it will + call that function and repeat this process until a non-callable is returned. + This can be useful to write recursive functions without blowing the stack. + Example:: + + >>> from functools import trampoline, partial + >>> def count_to_a_million(i): + ... i += 1 + ... if i < 1000000: + ... return partial(count_to_a_million, i) + ... else: + ... return i + ... + >>> trampoline(count_to_a_million, 1) + 1000000 + + +Wrapper Functions +~~~~~~~~~~~~~~~~~~~~~ + .. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) Update a *wrapper* function to look like the *wrapped* function. The optional Index: Lib/test/test_functools.py =================================================================== --- Lib/test/test_functools.py (revision 88194) +++ Lib/test/test_functools.py (working copy) @@ -1,4 +1,5 @@ import functools +import operator import sys import unittest from test import support @@ -653,6 +654,76 @@ self.assertEqual(fib.cache_info(), functools._CacheInfo(hits=0, misses=0, maxsize=None, currsize=0)) +class TestFlip(unittest.TestCase): + def test_flip(self): + def myfunc(a, b): + return (a, b) + result = functools.flip(myfunc)(1, 2) + self.assertEqual(result, (2, 1)) + + def test_flip_with_more_args(self): + def myfunc(a, b, c): + return a, b, c + result = functools.flip(myfunc)(1, 2, 3) + self.assertEqual(result, (2, 1, 3)) + +class TestConst(unittest.TestCase): + def test_const(self): + myfunc = functools.const(1) + # pass in arbitrary arguments just to make sure we can handle those + self.assertEqual(myfunc(1, 2, foo="bar"), 1) + +class TestCompose(unittest.TestCase): + def setUp(self): + self.add_to_hello = functools.partial(operator.add, "Hello, ") + + def test_compose_twoarg(self): + hellocat = functools.compose(str, self.add_to_hello) + self.assertEqual(hellocat(1), "Hello, 1") + + def test_compose_threearg(self): + append_exclamation_point = (functools.partial(functools.flip(operator.add), "!")) + make_hello = functools.compose(str, self.add_to_hello, append_exclamation_point) + self.assertEqual(make_hello(1), "Hello, 1!") + +class TestIdentity(unittest.TestCase): + def test_identity_one_arg(self): + result = functools.identity(1) + self.assertEqual(result, 1) + + def test_identity_multi_arg(self): + result = functools.identity(1, 2) + self.assertEqual(result, (1, 2)) + + def test_identity_no_arg(self): + result = functools.identity() + self.assertEqual(result, ()) + +class TestTrampoline(unittest.TestCase): + def test_no_func(self): + # Test that if no function is given, trampoline is basically a glorified + # apply. + def myfunc(): + return 1 + self.assertEqual(functools.trampoline(myfunc), 1) + + def test_with_other_functions(self): + def myfunc1(): + return myfunc2 + + def myfunc2(): + return myfunc3 + + def myfunc3(): + return 1 + + self.assertEqual(functools.trampoline(myfunc1), 1) + + def test_with_args(self): + def myfunc(a): + return a + self.assertEqual(functools.trampoline(myfunc, 1), 1) + def test_main(verbose=None): test_classes = ( TestPartial, @@ -663,6 +734,11 @@ TestWraps, TestReduce, TestLRU, + TestFlip, + TestConst, + TestCompose, + TestIdentity, + TestTrampoline, ) support.run_unittest(*test_classes) Index: Lib/functools.py =================================================================== --- Lib/functools.py (revision 88194) +++ Lib/functools.py (working copy) @@ -202,3 +202,51 @@ return wrapper return decorating_function + +def flip(func): + """Returns a function that calls func flipping its first two arguments. + Note that the returned function will not accept keyword arguments.""" + @wraps(func) + def wrapper(arg1, arg2, *args): + return func(arg2, arg1, *args) + return wrapper + +def const(retval): + """Returns a function that always returns the same value, no matter what + arguments it is given.""" + def constfunc(*args, **kwargs): + return retval + return constfunc + +def compose(func1, *funcs): + """Compose several functions together.""" + class ComposedFunc(object): + _funcs = (func1,) + funcs + __doc__ = "Function composed from {0}".format(_funcs) + + def __call__(self, *args, **kwargs): + result = func1(*args, **kwargs) + for func in funcs: + result = func(result) + return result + + def __repr__(self): + return "".format(self._funcs) + return ComposedFunc() + +def identity(*args): + """Function that returns what is passed in. If one item is given, that item + will be returned. Otherwise, a tuple of the arguments will be passed in.""" + if len(args) == 1: + return args[0] + else: + return args + +def trampoline(func, *args, **kwargs): + """Calls func with the given arguments. If func returns another function, + it will call that function and repeat this process until a non-callable + is returned.""" + result = func(*args, **kwargs) + while callable(result): + result = result() + return result