flask的debug看起来还是很神奇的,可以在异常页面查看当前调用栈,且能够在当前栈内进行交互式会话用以调试。本文将会从python的REPL进行说明并延伸到flask。看看它的具体实现

基本概念

python的运行是讲源码进行编译,之后虚拟机执行。这2个步骤python都提供了接口,compile和exec。exec的执行会调用compile,我们一般不会主动去调用compile。compile这个函数有三个模式官方文档 stackoverflow

  • exec:一系列声明
  • eval:单条表达式
  • single:单条声明,和exec不同的是当返回不为None的时候执行的时候会打印。这一想都知道专门是为交互式环境准备的

编译之后不用说。使用内建函数exec执行就好啦。它带有2个可选参数,全局空间变量及局部空间变量。看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
local = {}
code = compile('a=1', filename='<string>', mode='single')
print(code)
exec(code,local)
code = compile('a+=1', filename='<string>', mode='single')
try:
exec(code)
except:
print('Error')
exec(code,local)
print(local.get('a'))
code = compile('x+=1',filename='<string>',mode='single')
print(code)
# <code object <module> at 0x10fd9dc90, file "<string>", line 1>
# Error
# 2
# <code object <module> at 0x105c25c90, file "<string>", line 1>
  1. 编译的时候只要语法没有问题就可以通过,看x+=1,即使全局变量不存在x也能够编译通过
  2. 使用exec的时候,可以引入一个字典,否则会自动使用默认的global,有了这个字典我们就可以连接上下文,想一想。要是我们执行a=1,如果没有记录。那么后面执行a+=1就会报错了,这肯定不是我们所希望的

自带REPL

要实现一个类似的IDLE环境2条语句就够了

1
2
import code
code.interact()

看调用图
code_interactiveconsole
会涉及到2个模块,code和codeop,这2个模块可以都说是为生成交互式解释器服务的。code模块作用是接收请求然后解析再执行(exec)。解析部分就是由codeop提供的。设想一下为什么有了compile还需要一个单独的codeop模块。
我们使用交互式解释器的时候并不是所有的命令都是一行输入完成的。比如输入一个函数,那就需要多行。将我们输入的多行编译一次。这是我们的需求。codeop就是为这个而存在的。比如你执行codeop.compile_command(‘math(‘)会返回None。这就代表你下一行继续输入,直到你连续输入了2个空行。好了,它知道你输入完成了。核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def runsource(self, source, filename="<input>", symbol="single"):
try:
code = self.compile(source, filename, symbol)
except (OverflowError, SyntaxError, ValueError):
# Case 1
self.showsyntaxerror(filename)
return False

if code is None:
# Case 2
return True

# Case 3
self.runcode(code)
return False

逻辑还是比较好理解的。

Exception,traceback,frame联系

上个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import sys
class CustomeExcept(Exception):
def __init__(self, desc):
super(CustomeExcept, self).__init__()
self.desc = desc

def __repr__(self):
return self.desc

__str__ = __repr__

def t():
raise CustomeExcept('l')

def app():
a = 1
t()
try:
app()
except Exception as e:
# traceback.print_exc(file=sys.stdout)
exc_type, exc_value, exc_tb = sys.exc_info()
print(exc_tb.tb_frame.f_lineno)
print(exc_type)
print(exc_value is e)
print(exc_value)
print([i for i in dir(exc_tb) if not i.startswith('_')])
print(exc_tb.tb_frame, exc_tb.tb_lineno, exc_tb.tb_next.tb_lineno, exc_tb.tb_next.tb_next.tb_lineno,
exc_tb.tb_next.tb_next.tb_next, sys._getframe(0), exc_tb.tb_frame.f_back)
print(exc_tb.tb_next.tb_frame.f_locals)
# 23
# <class '__main__.CustomeExcept'>
# True
# l
# ['tb_frame', 'tb_lasti', 'tb_lineno', 'tb_next']
# <frame object at 0x7f90b5813958> 19 17 13 None <frame object at 0x7f90b5813958> None
# {'a': 1}
  • Exception配合raise使用,使用except捕获异常的时候返回它的实例
  • 返回的实例是和帧栈Frame是没有关系的
  • sys.exc_info返回了异常类,异常实例,traceback实例
  • traceback对象记录的帧不是当前帧,而是从发起异常的那个函数开始记录帧栈,因此比当前帧栈更长,traceback的中文意思是回溯,它的tb_next可以想象一下,实际上更可以说是读取上一个帧(因为引发异常的那个帧实际上在最上面)
  • tb_next返回的还是一个traceback对象
  • traceback模块提供了一些接口(比如traceback.print_exc)从帧中提取出信息然后格式化打印
    偷一张图(引用自http://busuncle.github.io/python-vm-and-bytecode.html)
    pyframeobject

werkzeug实现

1
2
3
4
__debugger__:yes
cmd:a = 1+1
frm:0
s:3WcibzVj8YXQDR0Na4bv

参考资料

Python虚拟机与字节码