EBNF属于“非专业人士无需了解的话题”,实际上,即便是我这种自认为是专业人士的老码农,在python的世界里浸淫了5年之久,也是最近才知道这个东西。
我们在编辑器里写好代码以后,如果代码里有语法错误,编辑器会立刻提醒,你有没有想过编辑器是如何检查出你的代码里有语法错误的呢,难道是后台偷偷运行来检测是否有语法错误?
最近阅读了一个开源项目的源码---https://github.com/davidhalter/parso, 简单一点描述这个项目的功能,那就是检查不同python版本代码里的语法错误,是的,你没看错,它可以检查不同版本下python代码的语法错误。
我很好奇,它是如何做到的,在回答这个问题之间,先根据官方提供的示例代码,检验一下它能否检测代码里的语法错误。
我先编写一个名为app.py的脚本,内容为
if 4 = 5:
print('ok')
这段代码里有很多初学者非常容易犯的错误,if语句里应当用==的地方,写成了赋值语句,只用了一个等号,接下来,写一段检测语法错误的代码
import parso
grammar = parso.load_grammar(version='3.6')
with open('app.py')as f:
text = f.read()
module = grammar.parse(text)
expr = module.children[0]
error_lst = grammar.iter_errors(module)
for err in error_lst:
print(err.message, err.code)
print(err.start_pos, err.end_pos)
程序输出结果
SyntaxError: invalid syntax 901
(1, 5) (1, 6)
IndentationError: unexpected indent 903
(2, 0) (2, 4)
效果很好,不只是检测出了语法错误,还清楚的标记出错误的位置,第1行从第5列至第6列,正是等号的位置,第2行0到4列,正是python所要求的4个空格所在的位置,由于if语句存在语法错误,因此这里也被判断为存在语法错误。
它是如何做到的呢?
跟踪函数load_grammar
def load_grammar(**kwargs):
def load_grammar(language='python', version=None, path=None):
if language == 'python':
version_info = parse_version_string(version)
file = path or os.path.join(
'python',
'grammar%s%s.txt' % (version_info.major, version_info.minor)
)
global _loaded_grammars
path = os.path.join(os.path.dirname(__file__), file)
try:
return _loaded_grammars[path]
except KeyError:
try:
with open(path) as f:
bnf_text = f.read()
grammar = PythonGrammar(version_info, bnf_text)
return _loaded_grammars.setdefault(path, grammar)
except FileNotFoundError:
message = "Python version %s is currently not supported." % version
raise NotImplementedError(message)
else:
raise NotImplementedError("No support for language %s." % language)
return load_grammar(**kwargs)
注意这行代码
path = os.path.join(os.path.dirname(__file__), file)
path是由parso所在的安装位置和python版本号拼出来的文件名称连接在一起的,通过调试,这个文件的绝对路径为
/Users/kwsy/anaconda3/lib/python3.8/site-packages/parso/python/grammar36.txt
在同级目录下,还有其他python版本的txt文件
这难道就是它可以检查不同版本python代码语法错误的凭借和依据么?打开文件,映入眼帘的是一堆完全看不懂的,很像正则表达式的东西
38 stmt: simple_stmt | compound_stmt | NEWLINE
39 simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
40 small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
41 import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
42 expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
43 ('=' (yield_expr|testlist_star_expr))*)
44 annassign: ':' test ['=' test]
45 testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
46 augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
47 '<<=' | '>>=' | '**=' | '//=')
48 # For normal and annotated assignments, additional restrictions enforced by the interpreter
49 del_stmt: 'del' exprlist
50 pass_stmt: 'pass'
51 flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
52 break_stmt: 'break'
循着文件最开头给出的提示信息,我打开了下面这个网址: https://docs.python.org/devguide/grammar.html , 了解到,这个文件里的就是EBNF。
EBNF的全称是Extended Backus-Naur Form, 扩展的BNF,BNF是巴科斯-诺尔范式,百度百科里对巴科斯范式做如下定义:
巴科斯范式以美国人巴科斯(Backus)和丹麦人诺尔(Naur)的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。
编辑器想要检查你的python代码是否符合语法,那么首先,它就需要先拿到一份正确的语法规则,这份语法规则就是python的EBNF。想要理解它,并不是非常的困难,因为它和正则表达式确实有点像,如下是它的语法标注符号体系
对语法标注符号有了了解后,我尝试解读它是如何对if语句做出语法规范的,我从网站上找出和if语句相关的规则
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
符合要求的if语句,在语法上应满足3个条件
那么 test又是如何定义的呢?
test: or_test ['if' or_test 'else' test] | lambdef
or_test: and_test ('or' and_test)*
and_test: not_test ('and' not_test)*
not_test: 'not' not_test | comparison
comparison: expr (comp_op expr)*
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
上面贴出来的,是和test有关的语法规则,你在理解的时候,可以遵循下面的顺序:test <= or_test <= and_test <= not_test <= comparison <= comp_op , 一路追踪,找到了comp_op,这里定义的都是比较运算符,你在if语句中,只能使用比较运算符,而赋值运算符(一个等号)是不符合语法规范的。
这份文件使用巴科斯范式定义的python语法,理论上,只要你算法能力足够强,你就可以自己写一个项目实现对python代码的语法检查。parso这个库所做的,就是实现这个功能,它能够检查不同版本python代码语法错误的秘密就在于它自身集成了不同版本的语法规范,并实现了依据EBNF对代码进行检查的算法。parso可以作为编辑器的插件来使用,检查语法错误,官网上介绍,它已经在jedi里被应用。
感兴趣的话,你可以去阅读parso的代码,坦率的讲,我只看了一小部分,就决定放弃了,毕竟这玩意和编译原理能扯上关系,能力不够啊!
QQ交流群: 211426309