打印函数的局部变量名和值
为了帮助我调试自己写的代码,我想做一个函数装饰器,这个装饰器可以在每次创建或修改变量时,打印出变量的名字和它的值。这样我就能清楚地看到在调用函数时发生了什么,就像在看比赛的实时解说一样。
之前我一直在用的方法是,在想要查看变量状态的地方加一行代码,比如 print(foo)
,但是这样做非常耗时,而且让我的代码看起来很乱(可能是最不符合Python风格的做法)。
我实际上想要的是这样的效果:
@show_guts
def foo(a,b):
biz = str(a)
baz = str(b)
return biz + baz
foo("banana","phone")
在IDE中打印出类似这样的内容:
biz = "banana"
baz = "phone"
bananaphone
我想知道 @show_guts
应该怎么写。我知道可以用一个装饰器只打印出 a
和 b
的值,像这样:
def print_args(function):
def wrapper(*args,**kwargs):
print("Arguments:",args)
print("Keyword Arguments:",kwargs)
return function(*args,**kwargs)
return wrapper
这样我就能得到:
Arguments: ('banana', 'phone')
Keyword Arguments: {}
'bananaphone'
但是我完全不知道怎么才能同时打印出局部变量的名字和它们的值,更别提要做到“整齐”了。
2 个回答
4
为了调试,我来分享我的小技巧:
import re
import inspect
assignment_regex = re.compile(r'(\s*)([\w\d_]+)\s*=\s*[\w\d+]')
def show_guts(fn):
source = inspect.getsource(fn)
lines = []
for line in source.split('\n'):
if 'show_guts' in line:
continue
lines.append(line)
if 'def' in line:
# kwargs will match the regex
continue
search = assignment_regex.search(line)
try:
groups = search.groups()
leading_whitespace = groups[0]
variable_name = groups[1]
lines.append(leading_whitespace + 'print "Assigning {0} =", {0}'.format(variable_name))
except AttributeError: # no match
pass
new_source = '\n'.join(lines)
namespace = {}
exec new_source in namespace
fn = namespace[fn.__name__]
def wrapped(*args, **kwargs):
arg_string = ', '.join(map(str, args))
kwarg_string = ', '.join(key + '=' + str(value) for key, value in kwargs.iteritems())
print "Calling", fn.__name__ + '(' + ', '.join((arg_string, kwarg_string)) + ')'
return fn(*args, **kwargs)
return wrapped
基本上,这个方法是自动化你之前手动做的事情。它会获取传入函数的源代码,逐行遍历这些代码,对于每一个赋值语句,它会创建一个新的打印语句,并把它加到源代码里。然后,新的源代码会被编译,原来的函数就会被新编译的函数替换掉。接着,为了获取 *args
和 **kwargs
,我创建了一个普通的装饰器包装函数,并加入了一些好看的打印语句。那部分可能用 inspect
模块会更好,但无所谓啦。
foo() -> 带打印语句的 foo()
# This...
@show_guts
def complicated(a, b, keyword=6):
bar = str(a)
baz = str(b)
if a == b:
if keyword != 6:
keyword = a
else:
keyword = b
return bar + baz
# becomes this
def complicated(a, b, keyword=6):
bar = str(a)
print "Assigning bar =", bar
baz = str(b)
print "Assigning baz =", baz
if a == b:
if keyword != 6:
keyword = a
print "Assigning keyword =", keyword
else:
keyword = b
print "Assigning keyword =", keyword
return bar + baz
用法
@show_guts
def foo(a, b):
bar = str(a)
baz = str(b)
return bar + baz
@show_guts
def complicated(a, b, keyword=6):
bar = str(a)
baz = str(b)
if a == b:
if keyword != 6:
keyword = a
else:
keyword = b
return bar + baz
foo(1, 2)
complicated(3, 4)
complicated(3, 3)
complicated(3, 3, keyword=123)
输出
Calling foo(1, 2, )
Assigning bar = 1
Assigning baz = 2
Calling complicated(3, 4, )
Assigning bar = 3
Assigning baz = 4
Assigning keyword = 4
Calling complicated(3, 3, )
Assigning bar = 3
Assigning baz = 3
Calling complicated(3, 3, keyword=123)
Assigning bar = 3
Assigning baz = 3
Assigning keyword = 3
可能还有一些特殊情况我在正则表达式中没有考虑到,但这个方法应该能让你接近目标。
14
你不能在不启用追踪的情况下做到这一点;这样会影响性能。函数的局部变量是在函数被调用时创建的,函数返回时会被清理,所以没有其他方法可以从装饰器中访问这些局部变量。
你可以使用 sys.settrace()
插入一个追踪函数,然后响应 Python 解释器发送给那个函数的事件。我们想要做的是只追踪被装饰的函数,并在函数返回时记录局部变量:
import sys
import threading
def show_guts(f):
sentinel = object()
gutsdata = threading.local()
gutsdata.captured_locals = None
gutsdata.tracing = False
def trace_locals(frame, event, arg):
if event.startswith('c_'): # C code traces, no new hook
return
if event == 'call': # start tracing only the first call
if gutsdata.tracing:
return None
gutsdata.tracing = True
return trace_locals
if event == 'line': # continue tracing
return trace_locals
# event is either exception or return, capture locals, end tracing
gutsdata.captured_locals = frame.f_locals.copy()
return None
def wrapper(*args, **kw):
# preserve existing tracer, start our trace
old_trace = sys.gettrace()
sys.settrace(trace_locals)
retval = sentinel
try:
retval = f(*args, **kw)
finally:
# reinstate existing tracer, report, clean up
sys.settrace(old_trace)
for key, val in gutsdata.captured_locals.items():
print '{}: {!r}'.format(key, val)
if retval is not sentinel:
print 'Returned: {!r}'.format(retval)
gutsdata.captured_locals = None
gutsdata.tracing = False
return retval
return wrapper
演示:
>>> @show_guts
... def foo(a,b):
... biz = str(a)
... baz = str(b)
... return biz + baz
...
>>> result = foo("banana","phone")
a: 'banana'
biz: 'banana'
b: 'phone'
baz: 'phone'
Returned: 'bananaphone'