Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I decorate a partially applied function in Python 3?

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.

like image 226
Jared Smith Avatar asked Feb 05 '23 00:02

Jared Smith


2 Answers

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))
like image 133
Aran-Fey Avatar answered Feb 07 '23 13:02

Aran-Fey


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 and Right 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

like image 36
Mulan Avatar answered Feb 07 '23 15:02

Mulan