1

As for example if want let decorated function see logger from closure

def logged(func):
    #here i want create logger which i want to be available from decorated function.
    @wraps(func)
    def _logged(*args, **kwargs):
        
        return func(*args,**kwargs)
        #naive idea hot to do it - obviously doesn't work
        #return exec('func(*args,**kwargs)',dict(func=func,logger=logger,args=args,kwargs=kwargs),dict(func=func,logger=logger,args=args,kwargs=kwargs))
    return _logged
  • 2
    Pass the logger to the decorated function as a parameter. Note that this requires that the decorated function accepts the logger argument (but the caller does not need to pass it). – Samwise Aug 29 '21 at 14:35

3 Answers3

2

Just make it a precondition for decorating a function that your function will receive a logger as an argument: it's just not something the result will take. (This is similar to how mock.patch works as a decorator.)

def logged(func):
    logger = ...

    @wraps(func)
    def _logged(*args, **kwargs):
        return func(*args, logger=logger, **kwargs):
    return _logged


@logged
def foo(x, y, *, logger):
    ...

foo(3, 5)

Despite appearances, logger is not a required keyword-only argument, because the name foo does not refer to your 3-argument function anymore. It refers to the function that logged creates, which will take care of passing the logger to your original foo when it finally gets called.

If you don't like having logger appear to be required, you can give it any default value you like, because that default value will never be used.

@logged
def foo(x, y, *, logger=None):
    ...

Note that while _logged is a closure, func is not, because closures deal with lexical scopes, and func was not defined in a lexical scope where logger was defined.

chepner
  • 497,756
  • 71
  • 530
  • 681
1

An alternative to making it seen via a closure would be to make it an attribute of the decorated function. Runnable example:

from functools import wraps


class Logger:
    def __call__(self, *args, **kwargs):
        return print(*args, **kwargs)


def logged(func):
    @wraps(func)
    def _logged(*args, **kwargs):
        return func(*args,**kwargs)

    _logged.logger = Logger()  # Make attribute of wrapped function.

    return _logged


@logged
def myfunc():
    myfunc.logger('Logged from myfunc')


myfunc()  # -> Logged from myfunc
martineau
  • 119,623
  • 25
  • 170
  • 301
0

Here's (another) way to do it, but again it's not through a closure which are determined at compile time, so can't be added at runtime. This version adds a global variable to the function's global namespace, taking care to save and restore the value of any similarly named variable that was already there. I got the idea from @Martijn Pieters' answer to the somewhat related question: How to inject variable into scope with a decorator?

from functools import wraps


class Logger:
    def __call__(self, *args, **kwargs):
        return print(*args, **kwargs)


def logged(func):
    logger = Logger()  # Get or create value to be added.
    varname = 'logger'
    sentinel = object()

    @wraps(func)
    def _logged(*args, **kwargs):
        namespace = func.__globals__
        oldvalue = namespace.get(varname, sentinel)  # Save prev value.
        namespace[varname] = logger

        try:
            return func(*args, **kwargs)
        finally:
            if oldvalue is sentinel:
                del namespace[varname]
            else:
                namespace[varname] = oldvalue

    return _logged


@logged
def myfunc():
    logger('Logged from myfunc.')


myfunc()  # -> Logged from myfunc.

martineau
  • 119,623
  • 25
  • 170
  • 301