冷门话题,聊一聊python的EBNF

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。想要理解它,并不是非常的困难,因为它和正则表达式确实有点像,如下是它的语法标注符号体系

  • : 表示定义
  • [ ] 中是可选项
  • ' ' 引号里的内容表示字符
  • | 竖线两边的是可选内容,相当于or
  • * 表示零个或者多个
  • + 表示一个或者多个

对语法标注符号有了了解后,我尝试解读它是如何对if语句做出语法规范的,我从网站上找出和if语句相关的规则

if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]

符合要求的if语句,在语法上应满足3个条件

  1. if后面紧跟一个表达式
  2. elif可以有0个或者多个
  3. else可有可无

那么 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

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案