调试:如何获取调用函数的文件名和行号?

72

我目前正在使用Python构建一个相当复杂的系统,在调试时,我通常会在多个脚本中放置简单的打印语句。为了保持概览,我也经常想要打印出打印语句所在的文件名和行号。我可以手动完成,或者使用类似下面这样的代码:

from inspect import currentframe, getframeinfo

print getframeinfo(currentframe()).filename + ':' + str(getframeinfo(currentframe()).lineno) + ' - ', 'what I actually want to print out here'

这将会打印出类似于:

filenameX.py:273 - what I actually want to print out here
为了让它更简单,我希望能够像这样做一些事情:
print debuginfo(), 'what I actually want to print out here'

于是我将它放到某个函数中并尝试执行:

from debugutil import debuginfo
print debuginfo(), 'what I actually want to print out here'
print debuginfo(), 'and something else here'

不幸的是,我得到:

debugutil.py:3 - what I actually want to print out here
debugutil.py:3 - and something else here

它打印出我定义函数的文件名和行号,而不是我调用debuginfo()的行号。这很明显,因为代码位于debugutil.py文件中。

所以我的问题实际上是:我如何获取调用此debuginfo()函数的文件名和行号?


2
使用 logging 模块并配置格式化程序:https://docs.python.org/2/library/logging.html#logrecord-attributes。Tornado Web 框架实际上具有非常类似的格式,因此您可以使用他们的代码:http://tornado.readthedocs.org/en/latest/_modules/tornado/log.html#LogFormatter - Blender
7个回答

114

函数 inspect.stack() 返回一个包含frame records的列表,从调用者开始向外移动,您可以使用它来获取所需的信息:

from inspect import getframeinfo, stack

def debuginfo(message):
    caller = getframeinfo(stack()[1][0])
    print("%s:%d - %s" % (caller.filename, caller.lineno, message)) # python3 syntax print

def grr(arg):
    debuginfo(arg)      # <-- stack()[1][0] for this line

grr("aargh")            # <-- stack()[2][0] for this line

输出:

example.py:8 - aargh

2
谢谢你的帮助!我将在我正在进行的几乎每个项目中使用它!您,先生,让我的生活变得更好了,现在我可以正式称之为“棒极了”! - kramer65
1
太好了!在 Django 中有一个函数,我无法弄清它是从哪里调用的。 - Sankalp
1
很棒的答案。这正是我正在寻找的。关于stack()的技巧很好......谢谢! - Hara
谢谢!我也使用了内省,但使用一个可用的补丁非常快。如果您包括以下内容,将会更好:from __future__ import print_function, absolute_import并且不使用打印语句。 - Christian Tismer
我把这个放在a.py里,然后在b.py中运行grr()。控制台输出a.py作为路径。 - ming
1
我对此的看法是使用f-strings(Python 3.6+),我发现语法更清晰易懂。另外,我使用os.path.basename,因为我不需要完整的路径名。 print(f'{os.path.basename(caller.filename)}:{caller.lineno} - {message}') - synkro

8
如果你把跟踪代码放在另一个函数中,并从主代码调用该函数,则需要确保从祖父级获取堆栈信息,而不是父级或跟踪函数本身。
以下是一个三层深的系统示例,以进一步说明我的意思。我的主函数调用一个跟踪函数,该函数又调用另一个函数来执行工作。
######################################

import sys, os, inspect, time
time_start = 0.0                    # initial start time

def trace_libary_init():
    global time_start

    time_start = time.time()      # when the program started

def trace_library_do(relative_frame, msg=""):
    global time_start

    time_now = time.time()

        # relative_frame is 0 for current function (this one), 
        # 1 for direct parent, or 2 for grand parent.. 

    total_stack         = inspect.stack()                   # total complete stack
    total_depth         = len(total_stack)                  # length of total stack
    frameinfo           = total_stack[relative_frame][0]    # info on rel frame
    relative_depth      = total_depth - relative_frame      # length of stack there

        # Information on function at the relative frame number

    func_name           = frameinfo.f_code.co_name
    filename            = os.path.basename(frameinfo.f_code.co_filename)
    line_number         = frameinfo.f_lineno                # of the call
    func_firstlineno    = frameinfo.f_code.co_firstlineno

    fileline            = "%s:%d" % (filename, line_number)
    time_diff           = time_now - time_start

    print("%13.6f %-20s %-24s %s" % (time_diff, fileline, func_name, msg))

################################

def trace_do(msg=""):
    trace_library_do(1, "trace within interface function")
    trace_library_do(2, msg)
    # any common tracing stuff you might want to do...

################################

def main(argc, argv):
    rc=0
    trace_libary_init()
    for i in range(3):
        trace_do("this is at step %i" %i)
        time.sleep((i+1) * 0.1)         # in 1/10's of a second
    return rc

rc=main(sys.argv.__len__(), sys.argv)
sys.exit(rc)

这将会打印出类似于以下内容的结果:
$ python test.py 
    0.000005 test.py:39           trace_do         trace within interface func
    0.001231 test.py:49           main             this is at step 0
    0.101541 test.py:39           trace_do         trace within interface func
    0.101900 test.py:49           main             this is at step 1
    0.302469 test.py:39           trace_do         trace within interface func
    0.302828 test.py:49           main             this is at step 2

顶部的trace_library_do()函数是一个示例,您可以将其放入库中,然后从其他跟踪函数中调用它。相对深度值控制打印哪个Python堆栈条目。

我在该函数中展示了一些其他有趣的值,比如函数起始行号、总堆栈深度和文件的完整路径。虽然我没有展示,但函数中的全局变量和本地变量也可以在inspect中使用,以及所有低于你的其他函数的完整堆栈跟踪。以上所示的信息已经足够生成分层调用/返回时间跟踪了。从这里开始创建自己源级别的调试器并不需要太多,而且大部分都已经准备好等待使用。

我相信会有人反对我使用inspect结构返回的数据内部字段,因为可能会有访问函数可以为您完成相同的事情。但我通过在Python调试器中步进此类型的代码来找到它们,并且它们至少在这里工作。我正在运行Python 2.7.12,如果您运行不同版本,则结果可能会有所不同。

无论如何,我强烈建议您将inspect代码导入自己的Python代码中,并查看它可以为您提供什么——特别是如果您可以在良好的Python调试器中单步执行代码。您将了解Python的工作原理,并看到语言的好处以及使其成为可能的背后发生的事情。

具有时间戳的完整源级跟踪是增强您对代码在动态实时环境中执行的理解的绝佳方式。这种类型的跟踪代码的好处在于,一旦编写完成,您就无需调试器支持即可查看它。


一开始,我无法分辨这是Python还是汇编语言。 - user66081

4

使用字符串插值和显示调用者函数名称的更新答案。

import inspect
def debuginfo(message):
    caller = inspect.getframeinfo(inspect.stack()[1][0])
    print(f"{caller.filename}:{caller.function}:{caller.lineno} - {message}")

4

traceprint 包现在可以为您完成此操作:

import traceprint

def func():
    print(f'Hello from func')

func()

#   File "/traceprint/examples/example.py", line 6, in <module>
#   File "/traceprint/examples/example.py", line 4, in func
# Hello from func

PyCharm会自动使文件链接可点击/可跟随。

通过pip install traceprint进行安装。


1

将您发布的代码放入一个函数中:

from inspect import currentframe, getframeinfo

def my_custom_debuginfo(message):
    print getframeinfo(currentframe()).filename + ':' + str(getframeinfo(currentframe()).lineno) + ' - ', message

然后按照您的意愿使用它:
# ... some code here ...
my_custom_debuginfo('what I actually want to print out here')
# ... more code ...

我建议您将该函数放在一个单独的模块中,这样每次需要使用它时都可以重复使用。


1
这就是我已经做的,然后它会打印出函数所在模块的文件名和行号,而不是我调用函数的位置。还有其他想法吗? - kramer65

1
我发现这个问题与我的问题有些类似,但我想要更多关于执行过程的细节(而且我不想安装整个调用图包)。
如果你想要更详细的信息,可以使用标准库模块 traceback 检索完整的回溯,并使用 traceback.extract_stack() 将堆栈对象(元组列表)存储起来,或者使用 traceback.print_stack() 打印出来。这对我的需求更加适合,希望能帮到其他人!

1

你可以使用Python内置的logging模块的findCaller findCaller函数,并将stacklevel 参数设置为2

E.x.:

import logging

def get_caller_info():
    file_name, line_no, func_name, stack_info = logging.root.findCaller(stacklevel=2)
    print(f"{dict(file_name=file_name,line_no=line_no,func_name=func_name,stack_info=stack_info,)}")


if __name__ == "__main__":
    get_caller_info()

将输出

{'file_name': 'C:\code\test_caller_info.py', 'line_no': 9, 'func_name': '', 'stack_info': None}


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接