I created a decorator factory that is parameterized by a custom logging function like so:
def _log_error(logger):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
try:
return f(*args, **kwargs)
except Exception as e:
logger(e)
return None
return wrapper
return decorator
Which I now want to use to decorate a partially-applied function foo:
foo = partial(bar, someparam)
I've tried all of the following:
@_log_error(logger)
foo = partial(bar, someparam)
log_error = _log_error(logger)
@log_error
foo = partial(...)
foo = partial(...)
@log_error
foo
@log_error
(foo = partial(...))
AFAICT both log_error = _log_error(logger) / @log_error
and @_log_error(logger)
seem totally valid ways of producing the decorator and it works fine on normally declared functions. But when trying to use on the partially applied function I get syntax errors at the start of foo =
, and googling while yielding excellent resources on working with decorators and functools.partial
in general have not given me anything on this specific case.
Decorators don't work on assignments. But since using a decorator is the same thing as calling the decorator, you can do
foo = _log_error(logger)(partial(bar, someparam))
Either way works
Here's another way you can do it using Either
– This answer gets its inspiration from Brian Lonsdorf's egghead series: Professor Frisby Introduces Composable Functional JavaScript
We'll take some of what we learned there and write some super sweet functional python codes
class Map (dict):
def __init__(self, **xw):
super(Map, self).__init__(**xw)
self.__dict__ = self
def Left (x):
return Map(
fold = lambda f, g: f(x),
bimap = lambda f, g: Left(f(x))
)
def Right (x):
return Map(
fold = lambda f, g: g(x),
bimap = lambda f, g: Right(g(x))
)
Note: This is a very incomplete implementation of
Left
andRight
but it's enough to get this specific job done. To take advantage of the full power of this super-powered data type, you'll want a complete implementation.
Generics promote code reuse
We'll setup a few more generic functions
def identity (x):
return x
def try_catch (f):
try:
return Right(f())
except Exception as e:
return Left(e)
def partial (f, *xs, **xw):
def aux (*ys, **yw):
return f(*xs, *ys, **xw, **yw)
return aux
Now we have enough to define log_error
– the syntax is a little wonky for writing curried functions in Python, but everything works as expected.
In plain English: we try
applying f
and get back a value. If the value is an error (Left
), call logger
, otherwise return the value (identity
)
def log_error (logger):
def wrapper (f):
def aux (*xs, **xw):
return try_catch (lambda: f(*xs, **xw)).bimap(logger, identity)
return aux
return wrapper
Putting it all together
Now let's try it with a little function
def foo (x,y,z):
return (x + y) * z
What you wanted to do was wrap a partially applied function in your using your custom logger
foo_logger = log_error(lambda err: print("ERROR:" + str(err))) (partial(foo,'a'))
foo_logger('b',3).fold(print, print)
# ('a' + 'b') * 3
# 'ab' * 3
# => ababab
foo_logger(1,3).fold(print, print)
# ('a' + 1) * 3
# ERROR: Can't convert 'int' object to str implicitly
# => None
Understanding the results
As you can see, when there is no error present (Right
), evaluation just keeps on moving and the computed value is passed to print
.
When an error occurs (Left
), the logger picks it up and logs the error message to the console. Because the logging function has no return value, None
is passed along to print
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With