Wednesday, June 7, 2006

Introspecting python variables (to print a debug message)

I asked about introspecting python variables. I was looking for something similar to Tcl's upvar command. I received a couple of good responses. I didn't ever end up using them for debugging, but I sure learned a lot more about Python introspection than I knew previously.

Alex Mohr:
We have to remember that an arbitrary number of variables, all with different names, can refer to the same object. For example, 
foo = 'something'
bar = foo
Now suppose I say, printvars('message', bar). The printvars function will get a reference to the 'something' string, but it doesn't know that the name of the reference I called you with was 'bar' rather than 'foo'. This function will take your argument, and look through globals() and find all the names that refer to that object, and then print those out, but this is almost certainly slow, fragile and useless:


def printvars(msg, *args):
    print msg, ':',
    for arg in args:
        matches = []
        for name, val in globals().items():
            if arg is val:
                matches.append(name)
        for match in matches:            print match+',',
        print '=', repr(arg)+';',
    print ''

If I use this as with my hypothetical example above, it produces this output:
>>> printvars('message', bar)
message : bar, foo, = 'something' 
Erick Tryzelaar:
This is doable with scary python black magic:
import sys
def foo(msg, *args):
   caller = sys._getframe(1)
   print msg, ' '.join(['%s=%s' % (x, caller.f_locals[x]) for x in args])
x = 5
y = 6
foo('hello', 'x', 'y')
hello x=5 y=6
Lars Damerow:
I agree with Erick and Alex here -- you can't get away from passing the variable names in as strings, since Python just considers variable names to be string keys for the local and global scope dictionaries. I also second Erick's opinion that using sys._getframe is not a good idea. It may look redundant, but the simplest and most bulletproof approach is probably something like this:
def printvars(msg, *args):
    print msg, ' '.join(['%s=%s' % (x, v) for (x, v) in args])
>>> x = 1
>>> y = 2
>>> printvars("hello", ("x", x), ("y", y))
hello x=1 y=2
We could make that a lot cleaner if Python had macros, but it doesn't. If we knew that all of the desired variables are in the same dictionary (which includes locals() and globals()), we could obviously clean up the calls like this:
def printvars(msg, scope, *args):
    print msg, ' '.join(['%s=%s' % (x, scope[x]) for x in args])
>>> x = 1
>>> y = 2
>>> printvars("hello", locals(), "x", "y")
hello x=1 y=2
Jim Atkinson:
Although you cannot avoid giving the name as well as the value for the variable. You can use keyword syntax to pass them and avoid having to type quotes around the name.
def printvars(msg, **kw):
    print msg,
    keys = kw.keys()
    keys.sort()
    for key in keys:
    print "%s=%r", (key, kw[key])
    print

x = 1 
y = 2 
printvars ("hello", x=x, y=y)
hello x=1 y=2 
The only downside is that you cannot control the order of the variables, it is always alphabetical.

No comments: