在Python中实现Rust的Dbg

2020-12-12 00:35:54

Rust具有令人惊叹的dbg宏,可让您快速设置一个表达式打印机,该打印机也将放在源代码行中。它还返回表达式的值,因此您甚至可以在需要时轻松地内联打印!

令a = 2;让b = dbg! (a * 2)+1; // ^-打印:[src / main.rs:2] a * 2 = 4 assert_eq! (b,5);

做一堆Python,我也希望在Python中做到这一点。我想要一个调试宏,它为我提供了文件位置+用于计算值的表达式。

这很烦人,因为您最终两次键入相同的表达式,无法内联它,而且看起来也不是很酷。

尝试实现此功能时,第一件事就是要接受Python缺少宏的情况。您不会真正弄混语法树,因此实际上只对计算出的值进行运算。因此,实际上很难在单个操作中捕获表达式及其结果,因为表达式会被求值。

因此,要打印some_expr =< some_expr的结果>一次不提供some_expr字符串。

但是,由于有了eval,您可以颠倒这个想法。您需要表达式的字符串,但是您不需要表达式本身!

向后退一步,现在向前两步。我们可以开始编写dbg helper函数

foo = {" bar" :3} def dbg(expr):result = eval(expr)print(f" {expr} = {result}")dbg(" foo [' bar' ]")#foo [' bar'] = 3

到目前为止,到目前为止,我们在这里有一台漂亮的漂亮打印机,但使用了两个引号。

事实证明,在不带参数的情况下调用eval时,我们将在dbg范围内进行评估。我们的示例之所以有效,是因为dbg是在表达式旁边定义的(因此foo在dbg中是可访问的),但是由于名称可以在不同的范围内定义,因此很容易出现名称阴影或无法计算表达式的情况

def dbg(expr):#我们在这里计算结果.... result = eval(expr)print(f" {expr} = {result}")#但实际上想要计算此范围内的结果dbg(" expr")

默认情况下,eval将计算当前帧内的表达式。但是我们可以传入自己的全局变量和局部变量来代替在任意环境中评估表达式

此模块提供四种主要服务:类型检查,获取源代码,检查类和函数以及检查解释器堆栈。

导入检查def dbg(expr):打印(检查。stack()[0]。code_context)结果= eval(expr)打印(f" {expr} = {结果}")返回结果dbg( " foo [' bar')expr = 5 dbg(" expr")如果dbg(" True"):打印(& #34;通过")

由于这是在函数调用中,因此我们实际上需要将堆栈上移一帧(inspect.stack()[1])以获取正确的行:

好的,现在我们离需要的地方越来越近。堆栈中的第二个元素是我们还将获取文件路径和行号的位置。让我们清理一下,现在我们得到:

这里我们有完整的文件路径...在这里可能有人想办法坚持使用文件名:

def dbg(expr):frame =检查。 stack()[1]。框架文件名=框架。文件名 。 split(" /")[-1] print(f" [{文件名}:{frame。lineno}] {expr} = {result}")返回结果

好的,这样对格式化很有好处。但是我们还没有修复评估!这是一个小小的解决方案:

def dbg(expr):frame =检查。 stack()[1]。 frame result = eval(expr,frame.f_globals,frame.f_locals)print(f" {expr} = {result}")返回结果

这为我们提供了正确的值,可以从调用范围而不是dbg正确评估expr:

框架上的f_globals和f_locals仍然是我最喜欢的Python功能之一。基本上,任何人都可以编写非常好的调试工具,而无需做很多魔术。

另外,如果您是IPython用户,则可以在〜/ .ipython / profile_default /中将其添加到启动脚本中,并在任何交互式shell中使用它。

对于那些珍视正确性,性能和知识完整性等愚蠢事物的人,您的旅程就此结束。这是一个很好的小帮手,可以在您的项目中使用,这里什么也看不到。

在框架内部,我们有一堆信息,但是对于探索性调试非常有用的一件事是code_context

现在,如果我们运行这样的函数,主体将接收foo [&bar]#的值,而不是表达式。但是由于有了堆栈,理论上我们可以恢复原始表达式!

如果您的源代码行带有dbg调用,那么如何恢复正在调试的表达式?

第一件事就是找到表达式的开头。相当简单,只需查找dbg(

因此,我们找到了表达式的开头,对吗?而且由于Python中的函数调用是一个开放括号,一些表达式,然后是一个封闭括号,因此我们只需要找到该封闭括号,就可以得到表示表达式的字符串片段。让我们在这里应用逻辑:

这是解析表达式的唯一方法是实际解析表达式的事情之一。简单的字符串匹配不会削减它,尤其是当您开始在混合函数中获取内部函数调用时。

您可以编写一个简单的解析器,尝试计算括号,打开引号,转义引号以及所有有趣的小游戏。但老实说,我想要一种可行的方法,并且我不想太想这件事。

ast。解析(' my_func(3,foo [" bar"] +"()"))== 5:' )#ast失败。解析(' my_func(3,foo [" bar"] +"()"))== 5' )#ast失败。解析(' my_func(3,foo [" bar"] +"()"))==' )#失败... ast。 parse(' my_func(3,foo [" bar"] +"()")')#不会失败=>我有我的表情!

我只是从尽可能长的字符串开始,然后慢慢缩小它,直到找到看起来像表达式的东西。因为debug调用中的)会使所有内容失去平衡,例如... 80%确保这种查找表达式的方式是正确的。

在某些情况下,使该策略不起作用。例如,如果您执行以下操作:

您在同一行上有两个dbg,因此很难知道您在哪个呼叫中,因为仅提供了行号。 (在其他一些项目上,我花了很多时间解决这个确切的问题,即使有诸如计数执行次数之类的技巧,也找不到解决方案。我相当确信这在某种程度上等同于停止问题)

老实说,我的解决方案未经考验,它感觉很不错,并且可以与我抛出的复杂表达式结合使用。但这是一件有趣的事情,现在可以进行以下工作:

dbg(foo [" bar"])#[pyhelpers.py:49] foo [" bar"] = 3如果dbg(my_func(3,foo [" bar&# 34;] +"()"))== 5:#[pyhelpers.py:52] my_func(3,foo [" bar"] +"() ")= 5次打印(" Success")

没有更多的愚蠢的傻瓜,没有更多的对eval的调用,只是一个真正愚蠢的解析技巧,除了我的直觉之外没有任何真正的基础。

def dbg(result):"""恢复给出结果的表达式,然后打印一条有用的调试语句,显示此"""框架=检查。 stack()[1] expr = extract_dbg(框架。code_context [0])文件名=框架。文件名 。分割(" /")[-1]打印(f" [{文件名}:{frame。lineno}] {expr} = {结果}")返回结果def extract_dbg (code_fragment):#从一行源文本中,尝试找到表达式#赋予对dbg的调用expression_options = code_fragment。 split(" dbg(")如果len(expression_options)!= 2:##如果有多个dbg语句#或我找不到dbg行,则保释金返回&#34 ;? ??"#获取dbg(expr_candidate = expression_options [1]的部分,而expr_candidate:try:ast。parse(expr_candidate)返回expr_candidate,但SyntaxError除外:expr_candidate = expr_candidate [:-1]#did&#39 ;找不到任何东西,也放弃回报" ???"