I use this quite often to analyze a python program to understand it better.
Consider the following piece of contrived python code to illustrate this approach of tracing python call flows.
def f():
f1('some value')
def f1(result):
print result
f2("f1 result")
def f2(result):
print result
f3("f2 result")
fe("f2 result")
return "f2 result"
def f3(result):
print result
return "f3 result"
def fe(result):
print result
f()
Output:
some value f1 result f2 result f2 result
To trace call flows, sys.settrace comes handy. You can pass your own function that can act as python source code debugger which is specific to your task. In my case, this is a function call tracer.
This function acts as a callback function for various events during execution of code. The function is called with the following arguments -frame, event, and arg. frame is the current stack frame. There are many events, I am particularly interested in 'call'. The callback is called with this event when a function call is made. I leveraged this to create a simple call flow. The output can be configured to generate visual graphs. The code below keeps it simple.
import sys
import os
import linecache
trace_depth = 0
def trace(f):
def globaltrace(frame, why, arg):
global trace_depth
if why == "call":
# function call event , extract information
.....
pass
trace_depth = trace_depth + 1
return None
def localtrace(frame, why, arg):
global trace_depth
if why == "line":
# line execution event
pass
elif why == "return":
trace_depth = trace_depth - 1
# function return event
return localtrace
def _f(*args, **kwds):
sys.settrace(globaltrace)
result = f(*args, **kwds)
sys.settrace(None)
return result
return _f
Here is a way to record function call event details.
if why == "call":
# Parent frame details
p_func = frame.f_back.f_code.co_name
p_file = frame.f_back.f_code.co_filename
p_lineinfo = frame.f_back.f_lineno
p_class = ''
p_module = ''
if frame.f_back.f_locals.has_key('self'):
p_class = frame.f_back.f_locals['self'].__class__.__name__
p_module = frame.f_back.f_locals['self'].__class__.__module__
# Current frame details
c_func = frame.f_code.co_name
c_file = frame.f_code.co_filename
c_lineinfo = frame.f_lineno
c_class = ''
c_module = ''
if frame.f_locals.has_key('self'):
c_class = frame.f_locals['self'].__class__.__name__
c_module = frame.f_locals['self'].__class__.__module__
# Order is Caller -> Callee
if trace_depth < 10:
print 'TraceInfo: ',
str(p_func).ljust(20),
'----->'.rjust(10) ,
str(c_func).rjust(10)
Now, if i want to checkout the call flow for function f1, then all I would need to do is to instrument it.
@trace
def f1(result):
print result
f2("f1 result")
The output is a pretty good result that shows a typical call flow.
TraceInfo: _f -----> f1 some value TraceInfo: f1 -----> f2 f1 result TraceInfo: f2 -----> f3 f2 result TraceInfo: f2 -----> fe f2 result
If you want to record line by line execution, publish it for 'line' event.
if why == "line":
# record the file name and line number of every trace
filename = frame.f_code.co_filename
lineno = frame.f_lineno
bname = os.path.basename(filename)
print "%s(%d): %s" % (bname, lineno,linecache.getline(filename, lineno)),
output:
callflow.py(79): print result
some value
callflow.py(80): f2("f1 result")
callflow.py(84): print result
f1 result
callflow.py(85): f3("f2 result")
callflow.py(90): print result
f2 result
callflow.py(91): return "f3 result"
callflow.py(86): fe("f2 result")
callflow.py(94): print result
f2 result
callflow.py(87): return "f2 result"
To look at the function return, you will have to work with 'return' event in localtrace.
elif why == "return":
trace_depth = trace_depth - 1
# Current frame details
c_func = frame.f_code.co_name
c_file = frame.f_code.co_filename
c_lineinfo = frame.f_lineno
c_class = ''
c_module = ''
if frame.f_locals.has_key('self'):
c_class = frame.f_locals['self'].__class__.__name__
c_module = frame.f_locals['self'].__class__.__module__
print "TraceInfo<---%s:%s:%s:%s:%s" % (c_file,
c_lineinfo,
c_module,
c_class,
c_func)
output:
some value f1 result f2 result TraceInfo<---callflow.py:91:::f3 f2 result TraceInfo<---callflow.py:94:::fe TraceInfo<---callflow.py:87:::f2 TraceInfo<---callflow.py:80:::f1
The above code can be morphed to provide a wide variety of information.
Pretty good entry and a handy utility. Thanks for putting this up.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDelete