3

I'm trying to write a function that helps me with debugging. Instead of inserting several print statements every now and then, I'd like to insert a one-liner debug statement. And what I'd expect to accomplish is that such function can be reused: I'd like to use it in several functions, in order to print different sets of variables.

In particular what I want to do is this:

def _debugFunction(var_list):
    global debug_flag
    if debug_flag:
        print 'START DEBUGGING:'
        < magic code >
        print 'END DEBUGGING'

def foo_function(n):
    x = 1
    y = 2
    z = 'string'
    debugFunction([x, y, z])
    return x + y

So that when I set debug_flag = True and call foo, the output is:

START DEBUGGING:
x = 1
y = 2
z = 'string'
END DEBUGGING
3

And then, if I set debug_flag = False and call foo, the output is:

3

In order to do that, I need to get, on runtime, the names of the variables I'm passing to debugFunction() as arguments, and print it along with their values.

I've searched through other posts and unable to find a direct answer.

How to get a variable name as a string in Python?

retrieving a variable's name in python at runtime?

From what I can actually understand, people are told to use the logging module. I've looked at it and tried to implement as much as a tiny piece of it, but I'm unable to log anything yet (will not surrender, though).

I've also read that people get pointed to __dict()__, vars(), or dir(), but just can't understand how to use them. Moreover: if I try to apply them to my variables I only get errors, or meaningless (to me ;) outputs.

Is there a way to do this? Is this TOO bad practice? If that's the case: what would be good practice?

PS: Perhaps there's no point in wasting efforts trying to implement this, but rather spend that time understanding how to properly use the logging module.

Community
  • 1
  • 1
abelinux
  • 133
  • 1
  • 5
  • What is `< magic code >`? – heinst Jul 24 '14 at 18:41
  • when calling debug function with list or arguments, they rare alredy resolved, so i htink you can't get variable names back at this point. you should change your api to provide pairs [['x',x],['y'],y],...] then caller is responsible to give variable names. – philippe lhardy Jul 24 '14 at 18:42
  • 1
    You should simply use a debugger like PyCharm or pdb if you'd like to step through your code. There might be a way to do what you're asking, but it'd be very complex and probably less effective than traditional debugging methods. – Chris Arena Jul 24 '14 at 18:47
  • Actually, you _could_ pass just the names or the values and access the other one with some ugly frame hacking, at least for CPython. But you shouldn't. – abarnert Jul 24 '14 at 18:55
  • @Chris Arena: for sure there're better ways to debug a function. I just don't know them =P. But taking into account your comment, I'll google 'PyCharm' and 'pdb' to change that ;) Thanks! – abelinux Jul 24 '14 at 19:11
  • 1
    @abelinux: `pdb` is built into Python; PyCharm is one of many IDEs that, among other cool features, wraps up debugging in a way that makes it more user-friendly. – abarnert Jul 24 '14 at 19:16

2 Answers2

3

I've also read that people get pointed to dict(), vars(), or dir(), but just can't understand how to use them.

Have you tried looking at the docs for these function? For example, vars:

… Without an argument, vars() acts like locals(). Note, the locals dictionary is only useful for reads since updates to the locals dictionary are ignored.

OK, so what does locals do? Look at the docs: it gives you a dictionary that maps each local name in your function to its value. So, if you don't want to pass both names and values, pass the locals() dictionary, and the names:

def foo_function(n):
    x = 1
    y = 2
    z = 'string'
    debugFunction(locals(), ['x', 'y', 'z'])
    return x + y

def _debugFunction(localdict, var_list):
    global debug_flag
    if debug_flag:
        print 'START DEBUGGING:'
        for name in var_list:
            print('{}: {}'.format(name, localdict[name]))
        print 'END DEBUGGING'

That's it. Except I might change the interface a little to either use *var_list or, even simpler, a string that I can split, and, to make it simpler to use in simple cases, default to printing all the locals:

def _debugFunction(localdict, var_list=''):
    var_list = var_list.split()
    if not var_list:
        var_list = localdict.keys()

Now you can do this:

_debugFunction(locals(), 'x y z')

Or just:

_debugFunction(locals())
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Thanks abarnert! Indeed, I had looked at the docs, but really couldn't understand them. And when tried vars(foobar), couldn't figure out the output... But your answer is EXACTLY what I was looking for. Again, thanks! – abelinux Jul 24 '14 at 19:05
  • @abelinux: `vars(foobar)` returns the members of `foobar`, which isn't what you want. It also only works if `foobar` is of a type that uses `__dict__` to store its members (to deal with that, try `inspect.getmembers(foobar)`), but you didn't want `foobar`'s members anyway. – abarnert Jul 24 '14 at 19:07
0

What you want is almost possible, for CPython only… but a really, really bad idea. Here's the code:

def _debugFunction(values):
    caller = sys._getframe(1)
    revlocals = {value: key for key, value in caller.f_locals.items()}
    for value in values:
        print('{}: {}'.format(revlocals[value], value))

The first problem here is that it only handles locals, not globals. That's easy to fix with, e.g., ChainMap or just dict.update.

The second problem is that if you pass it a value that isn't bound any name (like, say, _debugFunction([2]), it's going to raise an exception. You can fix that with, e.g., revlocals.get(value, '<constant>').

The third problem is that if you pass a value that's bound to two or more names, it's going to pick one arbitrarily. So this code is going to print 'x: 0' twice:

x = y = 0
_debugFunction([x, y])

You can even fix that one by looking into the frame's f_code and f_lasti to find the bytecode it was executing when it called your function, from which you can tell where each argument came from—e.g., if the first argument came from a LOAD_FAST 1, then the name is f.f_code.co_varnames[1]. (Alternatively, you can decompile it to an AST, which may be easier to process, but that requires third-party code.) This is even hackier—it relies on the fact that the CPython compiler never optimizes the argument passing in function calls in any way—which is almost always true, although I think there are two edge cases where it's not in Python 3.4. I'll leave the code, and discovering and working around those edge cases, as an exercise for the reader.

Of course turning it around and passing the names and using the frame hack just to get the values associated with those names is a lot simpler. Still a bad idea, and still hacky, but no so much that I think I should refuse to show the code:

def _debugFunction(names):
    f = sys._getframe(1)
    values = collections.ChainMap(f.f_locals, f.f_globals)
    for name in names.split():
        print('{}: {}'.format(name, values.get(name, '<undefined>')))

a = 0
def test(b):
    b = c = 1
    d = 'abc'
    _debugFunction('a b c d e')
abarnert
  • 354,177
  • 51
  • 601
  • 671
  • OK, now I'm confused... I thought your previous answer actually "did the trick", and pretty easily (i.e.: once you gave me the answer;), by the way. Now, this version, I assume it ends up printing the same (from looking at the proposed print ''.format() statement, but seems WAY MORE complicated... Is this kind-of-hacky workaround necessary? What's wrong with your previous answer? – abelinux Jul 24 '14 at 19:24
  • 1
    @abelinux: The only thing wrong with my previous answer is that it doesn't answer exactly the question you asked: you have to pass the locals explicitly, and you have to pass the variables by name rather than by value. I think it's a much _better_ answer than this one, as long as you're happy with those changes, but I wanted to show that it's not quite impossible to do what you literally asked for. – abarnert Jul 25 '14 at 21:33
  • 1
    @abelinux: Also, the very fact that it's so much more complicated for so little benefit should serve as an answer to anyone who comes along later and says "but I don't want to pass `locals()`, can't I do that automatically?" or "but I want to pass `x, y, z`, not `'x', 'y', 'z'`". – abarnert Jul 25 '14 at 21:34