4

I've found myself writing code like this several times:

def my_func(a, b, *args, **kwargs):
    saved_args = locals() # Learned about this from http://stackoverflow.com/a/3137022/2829764
    local_var = "This is some other local var that I don't want to log"
    try:
        a/b
    except Exception as e:
        logging.exception("Oh no! My args were: " + str(saved_args))
        raise

Running my_func(1, 0, "spam", "ham", my_kwarg="eggs") gives this output on stderr:

ERROR:root:Oh no! My args were: {'a': 1, 'args': (u'spam', u'ham'), 'b': 0, 'kwargs': {'my_kwarg': u'eggs'}}
Traceback (most recent call last):
  File "/Users/kuzzooroo/Desktop/question.py", line 17, in my_func
    a/b
ZeroDivisionError: division by zero

My question is, can I write something reusable so that I don't have to save locals() at the top of the function? And can it be done in a nice Pythonic way?

EDIT: one more request in response to @mtik00: ideally I'd like some way to access saved_args or the like from within my_func so that I can do something other than log uncaught exceptions (maybe I want to catch the exception in my_func, log an error, and keep going).

kuzzooroo
  • 6,788
  • 11
  • 46
  • 84
  • Really, really **DO NOT** use `locals()` to get the args. Not only is this almost impossible to read but it's also **not guaranteed to work**. Python implementations can chuck **whatever** they want in `locals` **whenever** they want. – Veedrac Jun 01 '14 at 18:56

1 Answers1

8

Decorators are what you are looking for. Here's an example:

import logging
from functools import wraps


def arg_logger(func):

    @wraps(func)
    def new_func(*args, **kwargs):
        saved_args = locals()
        try:
            return func(*args, **kwargs)
        except:
            logging.exception("Oh no! My args were: " + str(saved_args))
            raise

    return new_func


@arg_logger
def func(arg1, arg2):
    return 1 / 0

if __name__ == '__main__':
    func(1, 2)

Here, I'm using arg_logger() as a decorator. Apply the decorator to any function you want to have this new behavior.

There's a good discussion about decorators here.

mtik00
  • 147
  • 6
  • 1
    You just want `raise`, not `raise e`, to preserve the Traceback – dano Jun 01 '14 at 18:54
  • 3
    I believe `saved_args = locals()` should go on the inside of `new_func`. – Dair Jun 01 '14 at 18:55
  • 1
    `arg_logger()` will only ever receive a single argument, so you should remove `args` and `kwargs` there. As anon pointed out, you need to save the arguments within the wrapper function. – Sven Marnach Jun 01 '14 at 18:57
  • It doesn't actually matter if `save_args` goes inside or outside of `def new_func`, but it seems more logical to put it there, thanks. @sven: good call regarding `*args, **kwargs` on the decorator. – mtik00 Jun 01 '14 at 19:03
  • Thank you @mtik00. When I add a kw args with a default to func, via something like `def func(arg1, arg2, kw_with_default='spam')` then 'spam' (the default for kw_with_default) does not appear in the output, unlike with the code in the question. Can 'spam' be made to show up in the output? – kuzzooroo Jun 01 '14 at 19:08
  • Sorry, nevermind my last comment. I got it now. Thanks all for the input. – mtik00 Jun 01 '14 at 19:09
  • @kuzzooroo That's a lot more complicated since you are trying to inspect the original function's definition. You could probably do something with [inspect](https://docs.python.org/2/library/inspect.html#), but the solution looks pretty complex. – mtik00 Jun 01 '14 at 19:29
  • @mtik00, thank you for you answer. It's good although I am still hoping for something a little more general. I've clarified the requirements by added: "ideally I'd like some way to access saved_args or the like from within my_func so that I can do something other than log uncaught exceptions (maybe I want to catch the exception in my_func, log an error, and keep going)." – kuzzooroo Jun 01 '14 at 19:46
  • I don't quite follow. It sounds like you may need two different decorators? The idea is code re-use. It wouldn't make sense to create a decorator for only one function. I'm also not clear as to how my solution doesn't fit. Perhaps another example? As far as I can tell, you can change my `new_func()` to do whatever you need to do. Are you saying you want access to `saved_args` inside of `func()`? – mtik00 Jun 01 '14 at 22:18
  • "Are you saying you want access to `saved_args` inside of `func()`?" Yes, I guess I am, although it feels grubby--what if `func` happens to have a local variable called `saved_args`? We could (I think) make `saved_args` the first parameter in the call to `func`, but then we've hardly saved any typing. So I'm given your comment I'm curious about whether `saved_args` could be made available inside of `func` but maybe it's never advisable anyway. – kuzzooroo Jun 02 '14 at 15:43
  • @mtik00, I think that the code would benefit from a `@functools.wraps(func)` above the line `def new_func(*args, **kwargs):`. – kuzzooroo Jul 13 '14 at 22:08
  • @kuzzooroo, Sure, that wouldn't hurt. It's also needed if you're doing more inspection on the return function (e.g. func.__name__). – mtik00 Jul 30 '14 at 22:09