You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

506 lines
13 KiB

from functools import reduce, partial
import inspect
import operator
import sys
__all__ = ('identity', 'thread_first', 'thread_last', 'memoize', 'compose',
'pipe', 'complement', 'juxt', 'do', 'curry')
def identity(x):
""" Identity function. Return x
>>> identity(3)
3
"""
return x
def thread_first(val, *forms):
""" Thread value through a sequence of functions/forms
>>> def double(x): return 2*x
>>> def inc(x): return x + 1
>>> thread_first(1, inc, double)
4
If the function expects more than one input you can specify those inputs
in a tuple. The value is used as the first input.
>>> def add(x, y): return x + y
>>> def pow(x, y): return x**y
>>> thread_first(1, (add, 4), (pow, 2)) # pow(add(1, 4), 2)
25
So in general
thread_first(x, f, (g, y, z))
expands to
g(f(x), y, z)
See Also:
thread_last
"""
def evalform_front(val, form):
if callable(form):
return form(val)
if isinstance(form, tuple):
func, args = form[0], form[1:]
args = (val,) + args
return func(*args)
return reduce(evalform_front, forms, val)
def thread_last(val, *forms):
""" Thread value through a sequence of functions/forms
>>> def double(x): return 2*x
>>> def inc(x): return x + 1
>>> thread_last(1, inc, double)
4
If the function expects more than one input you can specify those inputs
in a tuple. The value is used as the last input.
>>> def add(x, y): return x + y
>>> def pow(x, y): return x**y
>>> thread_last(1, (add, 4), (pow, 2)) # pow(2, add(4, 1))
32
So in general
thread_last(x, f, (g, y, z))
expands to
g(y, z, f(x))
>>> def iseven(x):
... return x % 2 == 0
>>> list(thread_last([1, 2, 3], (map, inc), (filter, iseven)))
[2, 4]
See Also:
thread_first
"""
def evalform_back(val, form):
if callable(form):
return form(val)
if isinstance(form, tuple):
func, args = form[0], form[1:]
args = args + (val,)
return func(*args)
return reduce(evalform_back, forms, val)
# This is a kludge for Python 3.4.0 support
# currently len(inspect.getargspec(map).args) == 0, a wrong result.
# As this is fixed in future versions then hopefully this kludge can be
# removed.
known_numargs = {map: 2, filter: 2, reduce: 2}
def _num_required_args(func):
""" Number of args for func
>>> def foo(a, b, c=None):
... return a + b + c
>>> _num_required_args(foo)
2
>>> def bar(*args):
... return sum(args)
>>> print(_num_required_args(bar))
None
"""
if func in known_numargs:
return known_numargs[func]
try:
spec = inspect.getargspec(func)
if spec.varargs:
return None
num_defaults = len(spec.defaults) if spec.defaults else 0
return len(spec.args) - num_defaults
except TypeError:
return None
class curry(object):
""" Curry a callable function
Enables partial application of arguments through calling a function with an
incomplete set of arguments.
>>> def mul(x, y):
... return x * y
>>> mul = curry(mul)
>>> double = mul(2)
>>> double(10)
20
Also supports keyword arguments
>>> @curry # Can use curry as a decorator
... def f(x, y, a=10):
... return a * (x + y)
>>> add = f(a=1)
>>> add(2, 3)
5
See Also:
toolz.curried - namespace of curried functions
http://toolz.readthedocs.org/en/latest/curry.html
"""
def __init__(self, func, *args, **kwargs):
if not callable(func):
raise TypeError("Input must be callable")
# curry- or functools.partial-like object? Unpack and merge arguments
if (hasattr(func, 'func')
and hasattr(func, 'args')
and hasattr(func, 'keywords')
and isinstance(func.args, tuple)):
_kwargs = {}
if func.keywords:
_kwargs.update(func.keywords)
_kwargs.update(kwargs)
kwargs = _kwargs
args = func.args + args
func = func.func
if kwargs:
self._partial = partial(func, *args, **kwargs)
else:
self._partial = partial(func, *args)
self.__doc__ = getattr(func, '__doc__', None)
self.__name__ = getattr(func, '__name__', '<curry>')
@property
def func(self):
return self._partial.func
@property
def args(self):
return self._partial.args
@property
def keywords(self):
return self._partial.keywords
@property
def func_name(self):
return self.__name__
def __str__(self):
return str(self.func)
def __repr__(self):
return repr(self.func)
def __hash__(self):
return hash((self.func, self.args,
frozenset(self.keywords.items()) if self.keywords
else None))
def __eq__(self, other):
return (isinstance(other, curry) and self.func == other.func and
self.args == other.args and self.keywords == other.keywords)
def __ne__(self, other):
return not self.__eq__(other)
def __call__(self, *args, **kwargs):
try:
return self._partial(*args, **kwargs)
except TypeError:
# If there was a genuine TypeError
required_args = _num_required_args(self.func)
if (required_args is not None and
len(args) + len(self.args) >= required_args):
raise
return curry(self._partial, *args, **kwargs)
# pickle protocol because functools.partial objects can't be pickled
def __getstate__(self):
# dictoolz.keyfilter, I miss you!
userdict = tuple((k, v) for k, v in self.__dict__.items()
if k != '_partial')
return self.func, self.args, self.keywords, userdict
def __setstate__(self, state):
func, args, kwargs, userdict = state
self.__init__(func, *args, **(kwargs or {}))
self.__dict__.update(userdict)
def has_kwargs(f):
""" Does a function have keyword arguments?
>>> def f(x, y=0):
... return x + y
>>> has_kwargs(f)
True
"""
if sys.version_info[0] == 2: # pragma: no cover
spec = inspect.getargspec(f)
return bool(spec and (spec.keywords or spec.defaults))
if sys.version_info[0] == 3: # pragma: no cover
spec = inspect.getfullargspec(f)
return bool(spec.defaults)
def isunary(f):
""" Does a function have only a single argument?
>>> def f(x):
... return x
>>> isunary(f)
True
>>> isunary(lambda x, y: x + y)
False
"""
try:
if sys.version_info[0] == 2: # pragma: no cover
spec = inspect.getargspec(f)
if sys.version_info[0] == 3: # pragma: no cover
spec = inspect.getfullargspec(f)
return bool(spec and spec.varargs is None and not has_kwargs(f)
and len(spec.args) == 1)
except TypeError: # pragma: no cover
return None # in Python < 3.4 builtins fail, return None
@curry
def memoize(func, cache=None, key=None):
""" Cache a function's result for speedy future evaluation
Considerations:
Trades memory for speed.
Only use on pure functions.
>>> def add(x, y): return x + y
>>> add = memoize(add)
Or use as a decorator
>>> @memoize
... def add(x, y):
... return x + y
Use the ``cache`` keyword to provide a dict-like object as an initial cache
>>> @memoize(cache={(1, 2): 3})
... def add(x, y):
... return x + y
Note that the above works as a decorator because ``memoize`` is curried.
It is also possible to provide a ``key(args, kwargs)`` function that
calculates keys used for the cache, which receives an ``args`` tuple and
``kwargs`` dict as input, and must return a hashable value. However,
the default key function should be sufficient most of the time.
>>> # Use key function that ignores extraneous keyword arguments
>>> @memoize(key=lambda args, kwargs: args)
... def add(x, y, verbose=False):
... if verbose:
... print('Calculating %s + %s' % (x, y))
... return x + y
"""
if cache is None:
cache = {}
try:
may_have_kwargs = has_kwargs(func)
# Is unary function (single arg, no variadic argument or keywords)?
is_unary = isunary(func)
except TypeError: # pragma: no cover
may_have_kwargs = True
is_unary = False
def memof(*args, **kwargs):
try:
if key is not None:
k = key(args, kwargs)
elif is_unary:
k = args[0]
elif may_have_kwargs:
k = (args or None,
frozenset(kwargs.items()) if kwargs else None)
else:
k = args
in_cache = k in cache
except TypeError:
raise TypeError("Arguments to memoized function must be hashable")
if in_cache:
return cache[k]
else:
result = func(*args, **kwargs)
cache[k] = result
return result
try:
memof.__name__ = func.__name__
except AttributeError:
pass
memof.__doc__ = func.__doc__
return memof
class Compose(object):
""" A composition of functions
See Also:
compose
"""
__slots__ = ['funcs']
def __init__(self, *funcs):
self.funcs = funcs
def __call__(self, *args, **kwargs):
fns = list(reversed(self.funcs))
ret = fns[0](*args, **kwargs)
for f in fns[1:]:
ret = f(ret)
return ret
def __getstate__(self):
return self.funcs
def __setstate__(self, state):
self.funcs = tuple(state)
def compose(*funcs):
""" Compose functions to operate in series.
Returns a function that applies other functions in sequence.
Functions are applied from right to left so that
``compose(f, g, h)(x, y)`` is the same as ``f(g(h(x, y)))``.
If no arguments are provided, the identity function (f(x) = x) is returned.
>>> inc = lambda i: i + 1
>>> compose(str, inc)(3)
'4'
See Also:
pipe
"""
if not funcs:
return identity
if len(funcs) == 1:
return funcs[0]
else:
return Compose(*funcs)
def pipe(data, *funcs):
""" Pipe a value through a sequence of functions
I.e. ``pipe(data, f, g, h)`` is equivalent to ``h(g(f(data)))``
We think of the value as progressing through a pipe of several
transformations, much like pipes in UNIX
``$ cat data | f | g | h``
>>> double = lambda i: 2 * i
>>> pipe(3, double, str)
'6'
See Also:
compose
thread_first
thread_last
"""
for func in funcs:
data = func(data)
return data
def complement(func):
""" Convert a predicate function to its logical complement.
In other words, return a function that, for inputs that normally
yield True, yields False, and vice-versa.
>>> def iseven(n): return n % 2 == 0
>>> isodd = complement(iseven)
>>> iseven(2)
True
>>> isodd(2)
False
"""
return compose(operator.not_, func)
class juxt(object):
"""
Creates a function that calls several functions with the same arguments.
Takes several functions and returns a function that applies its arguments
to each of those functions then returns a tuple of the results.
Name comes from juxtaposition: the fact of two things being seen or placed
close together with contrasting effect.
>>> inc = lambda x: x + 1
>>> double = lambda x: x * 2
>>> juxt(inc, double)(10)
(11, 20)
>>> juxt([inc, double])(10)
(11, 20)
"""
__slots__ = ['funcs']
def __init__(self, *funcs):
if len(funcs) == 1 and not callable(funcs[0]):
funcs = funcs[0]
self.funcs = tuple(funcs)
def __call__(self, *args, **kwargs):
return tuple(func(*args, **kwargs) for func in self.funcs)
def __getstate__(self):
return self.funcs
def __setstate__(self, state):
self.funcs = state
def do(func, x):
""" Runs ``func`` on ``x``, returns ``x``
Because the results of ``func`` are not returned, only the side
effects of ``func`` are relevant.
Logging functions can be made by composing ``do`` with a storage function
like ``list.append`` or ``file.write``
>>> from toolz import compose
>>> from toolz.curried import do
>>> log = []
>>> inc = lambda x: x + 1
>>> inc = compose(inc, do(log.append))
>>> inc(1)
2
>>> inc(11)
12
>>> log
[1, 11]
"""
func(x)
return x