Skip to content

BNF 以及 EBNF

31 / 44

通常情况下你很少会在入门书籍里读到关于 Backus-Naur Form(BNF,巴科斯-诺尔范式 Extended Backus-Naur Form(EBNF)的话题 —— 它们都被普遍认为是非专业人士无需了解的话题”,隐含的另外一层含义是反正就算给他们讲他们也无论如何看不懂”……

然而在我眼里这事非讲不可 —— 这是这本的设计目标决定的

严格意义上来讲自学是门手艺以自学编程为例我完全没必要自己动手耗时费力写那么多东西 —— 如果仅仅是为了让读者入门的话编程入门书籍或者 Python 编程入门书籍都已经太多太多了其中质量过硬的书籍也多得去了 —— 并且如果你没有英文阅读障碍那你就会发现网上有太多非常优质的免费教程…… 真的轮不到李笑来同学再写一次

我写这本书的目标是

让读者从认知自学能力开始通过自学编程作为第一个实践逐步完整掌握自学能力进而在随后漫长的人生中需要什么就去学什么

…… 不用非得找人教找人带 —— 只有这样前途这两个字才会变得实在

于是我最希望能做到的是从这里了解了自学方法论也了解了编程以及 Python 编程的基础概念之后,《自学是门手艺的读者能够自顾自地踏上征程,一路走下去 —— 至于走到哪里能走到哪里不是我一个作者一厢情愿能够决定的是吧

当然会自学的人运气一定不会差

于是这本的核心目标之一换个说法就是

我希望读者在读完自学是门手艺之后有能力独立地去全面研读官方文档 —— 甚至是各种编程语言各种软件的相关的文档包括它们的官方文档)。

自学编程很像独自一人冲入了一个丛林里面什么动物都有…… 而且那个丛林很大很大虽然丛林里有的地方很美可若是没有地图和指南针你就会迷失方向

其实吧地图也不是没有 —— 别说 Python 无论什么编程语言包括无论什么软件都有很翔实的官方文档…… 可是吧绝大多数人无论买多少书上多少课就是不去用官方地图”,就不

—— 其实倒不是说第三方地图更好实际的原因很不好意思说出来

  • 这首先吧觉得官方文档阅读量太大了……(那地图不是越详细越好吗?)
  • 那还有吧…… 也不是没去看过看不懂……(…… 这对初学者倒是个问题!)

所以我要认为这本的最重要工作是

为读者解读清楚地图上的图例”,从此之后读者在任何需要的时候能够彻底读懂地图

在阅读官方文档的时候很多人在 The Python Tutorial 上就已经觉得吃力了…… 如果到了 Standard Libraries Language References 的部分就基本上完全放弃了比如以下这段摘自 string —— Common string operations

Format Specification Mini-Language ... The general form of a standard format specifier is:

plaintext
format_spec     ::=  [[fill]align][sign][#][0][width][grouping_option][.precision][type]
fill            ::=  <any character>
align           ::=  "<" | ">" | "=" | "^"
sign            ::=  "+" | "-" | " "
width           ::=  digit+
grouping_option ::=  "_" | ","
precision       ::=  digit+
type            ::=  "b" | "c" | "d" | "e" | "E" | "f" | "F" | "g" | "G" |
                     "n" | "o" | "s" | "x" | "X" | "%"

...

读到这看着一大堆的 ::= [] | 当场傻眼了……

这是 BNF 描述还是 Python 自己定制的 EBNF…… 为了理解它们以后当然最好有空研究一下上下文无关文法”(Context-free Grammar),没准未来你一高兴就会去玩一般人不敢玩的各种 Parser,甚至干脆自己写门编程语言啥的…… 不过完全可以跳过那些复杂的东西的 —— 因为你当前的目标只不过是能够读懂那些符号的含义”。

其实吧真的不难的 —— 它就是语法描述的方法

比如什么是符合语法的整数(Integer)符合以下语法描述的是整数使用 Python EBNF):

plaintext
integer ::= [sign] digit +
sign    ::= "+" | "-"
digit   ::=  "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

以上的描述中基本符号没几个它们各自的含义是

  • ::= 表示定义
  • < > 尖括号里的内容表示必选内容
  • [ ] 中是可选项
  • " " 双引号里的内容表示字符
  • | 竖线两边的是可选内容相当于or;
  • * 表示零个或者多个……
  • + 表示一个或者多个……

于是

  1. interger 定义是可选的 sign一个或者多个 digit 的集合构成 —— 第一行末尾那个 + 的作用和正则表达式里的 + 一样
  2. sign 的定义是什么呢要么是 + 要么是 -;
  3. digit 的定义是什么呢 "0" "9" 中的任何一个值……

于是99+99-99都是符合以上语法描述的 integer 99+ 99- 肯定不符合以上语法描述的 integer

很简单吧反正就是在 ::= 左边逐行列出一个语法构成的所有要素而后在右边逐行逐一定义直至全部要素定义完毕

也许那些在此之前已经熟悉 BNF 范式的人会有点惊讶,“你怎么连终结符非终结符这种最基本的概念都跳过了?” —— 是呀即便不讲那俩概念也能把这事讲清楚到能马上开始用了的地步…… 这就是我经常说的,“人类有这个神奇的本领擅长使用自己并不懂的东西……”

Python BNF 的拓展借鉴了正则表达式[1] —— 从最后两个符号的使用* +你可以看得出来顺带说这也是为什么这本里非要讲其他入门书籍里不讲的正则表达式的原因之一

又由于 Python 的社区文档是二十来年长期积累的有时标注方法并不一致比如在描述 Python Full Grammar specification 的时候他们用的语法标注符号体系就跟上面描述 String 的语法不一样了是这样的

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

—— 用冒号 : 替代了 ::=用单引号 '' 替代了双引号 ""而尖括号 <> 干脆不用了

python
# Grammar for Python

# NOTE WELL: You should also follow all the steps listed at
# https://devguide.python.org/grammar/

# Start symbols for the grammar:
#       single_input is a single interactive statement;
#       file_input is a module or sequence of commands read from an input file;
#       eval_input is the input for the eval() functions.
# NB: compound_stmt in single_input is followed by extra NEWLINE!
single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE
file_input: (NEWLINE | stmt)* ENDMARKER
eval_input: testlist NEWLINE* ENDMARKER

decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE
decorators: decorator+
decorated: decorators (classdef | funcdef | async_funcdef)

async_funcdef: 'async' funcdef
funcdef: 'def' NAME parameters ['->' test] ':' suite

parameters: '(' [typedargslist] ')'
typedargslist: (tfpdef ['=' test] (',' tfpdef ['=' test])* [',' [
        '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
      | '**' tfpdef [',']]]
  | '*' [tfpdef] (',' tfpdef ['=' test])* [',' ['**' tfpdef [',']]]
  | '**' tfpdef [','])
tfpdef: NAME [':' test]
varargslist: (vfpdef ['=' test] (',' vfpdef ['=' test])* [',' [
        '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
      | '**' vfpdef [',']]]
  | '*' [vfpdef] (',' vfpdef ['=' test])* [',' ['**' vfpdef [',']]]
  | '**' vfpdef [',']
)
vfpdef: NAME

stmt: simple_stmt | compound_stmt
simple_stmt: small_stmt (';' small_stmt)* [';'] NEWLINE
small_stmt: (expr_stmt | del_stmt | pass_stmt | flow_stmt |
             import_stmt | global_stmt | nonlocal_stmt | assert_stmt)
expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
annassign: ':' test ['=' test]
testlist_star_expr: (test|star_expr) (',' (test|star_expr))* [',']
augassign: ('+=' | '-=' | '*=' | '@=' | '/=' | '%=' | '&=' | '|=' | '^=' |
            '<<=' | '>>=' | '**=' | '//=')
# For normal and annotated assignments, additional restrictions enforced by the interpreter
del_stmt: 'del' exprlist
pass_stmt: 'pass'
flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt | yield_stmt
break_stmt: 'break'
continue_stmt: 'continue'
return_stmt: 'return' [testlist]
yield_stmt: yield_expr
raise_stmt: 'raise' [test ['from' test]]
import_stmt: import_name | import_from
import_name: 'import' dotted_as_names
# note below: the ('.' | '...') is necessary because '...' is tokenized as ELLIPSIS
import_from: ('from' (('.' | '...')* dotted_name | ('.' | '...')+)
              'import' ('*' | '(' import_as_names ')' | import_as_names))
import_as_name: NAME ['as' NAME]
dotted_as_name: dotted_name ['as' NAME]
import_as_names: import_as_name (',' import_as_name)* [',']
dotted_as_names: dotted_as_name (',' dotted_as_name)*
dotted_name: NAME ('.' NAME)*
global_stmt: 'global' NAME (',' NAME)*
nonlocal_stmt: 'nonlocal' NAME (',' NAME)*
assert_stmt: 'assert' test [',' test]

compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated | async_stmt
async_stmt: 'async' (funcdef | with_stmt | for_stmt)
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
for_stmt: 'for' exprlist 'in' testlist ':' suite ['else' ':' suite]
try_stmt: ('try' ':' suite
           ((except_clause ':' suite)+
            ['else' ':' suite]
            ['finally' ':' suite] |
           'finally' ':' suite))
with_stmt: 'with' with_item (',' with_item)*  ':' suite
with_item: test ['as' expr]
# NB compile.c makes sure that the default except clause is last
except_clause: 'except' [test ['as' NAME]]
suite: simple_stmt | NEWLINE INDENT stmt+ DEDENT

test: or_test ['if' or_test 'else' test] | lambdef
test_nocond: or_test | lambdef_nocond
lambdef: 'lambda' [varargslist] ':' test
lambdef_nocond: 'lambda' [varargslist] ':' test_nocond
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)*
# <> isn't actually a valid comparison operator in Python. It's here for the
# sake of a __future__ import described in PEP 401 (which really works :-)
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
star_expr: '*' expr
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom_expr ['**' factor]
atom_expr: ['await'] atom trailer*
atom: ('(' [yield_expr|testlist_comp] ')' |
       '[' [testlist_comp] ']' |
       '{' [dictorsetmaker] '}' |
       NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False')
testlist_comp: (test|star_expr) ( comp_for | (',' (test|star_expr))* [','] )
trailer: '(' [arglist] ')' | '[' subscriptlist ']' | '.' NAME
subscriptlist: subscript (',' subscript)* [',']
subscript: test | [test] ':' [test] [sliceop]
sliceop: ':' [test]
exprlist: (expr|star_expr) (',' (expr|star_expr))* [',']
testlist: test (',' test)* [',']
dictorsetmaker: ( ((test ':' test | '**' expr)
                   (comp_for | (',' (test ':' test | '**' expr))* [','])) |
                  ((test | star_expr)
                   (comp_for | (',' (test | star_expr))* [','])) )

classdef: 'class' NAME ['(' [arglist] ')'] ':' suite

arglist: argument (',' argument)*  [',']

# The reason that keywords are test nodes instead of NAME is that using NAME
# results in an ambiguity. ast.c makes sure it's a NAME.
# "test '=' test" is really "keyword '=' test", but we have no such token.
# These need to be in a single rule to avoid grammar that is ambiguous
# to our LL(1) parser. Even though 'test' includes '*expr' in star_expr,
# we explicitly match '*' here, too, to give it proper precedence.
# Illegal combinations and orderings are blocked in ast.c:
# multiple (test comp_for) arguments are blocked; keyword unpackings
# that precede iterable unpackings are blocked; etc.
argument: ( test [comp_for] |
            test '=' test |
            '**' test |
            '*' test )

comp_iter: comp_for | comp_if
sync_comp_for: 'for' exprlist 'in' or_test [comp_iter]
comp_for: ['async'] sync_comp_for
comp_if: 'if' test_nocond [comp_iter]

# not used in grammar, but may appear in "node" passed from Parser to Compiler
encoding_decl: NAME

yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

现在你已经能读懂 BNF 那么可以再读读用 BNF 描述的 Regex 语法[2]就当复习了 —— 很短的

html
BNF grammar for Perl-style regular expressions

<RE>             ::=  <union> | <simple-RE>
<union>          ::=  <RE> "|" <simple-RE>
<simple-RE>      ::=  <concatenation> | <basic-RE>
<concatenation>  ::=  <simple-RE> <basic-RE>
<basic-RE>       ::=  <star> | <plus> | <elementary-RE>
<star>           ::=  <elementary-RE> "*"
<plus>           ::=  <elementary-RE> "+"
<elementary-RE>  ::=  <group> | <any> | <eos> | <char> | <set>
<group>          ::=  "(" <RE> ")"
<any>            ::=  "."
<eos>            ::=  "$"
<char>           ::=  any non metacharacter | "\" metacharacter
<set>            ::=  <positive-set> | <negative-set>
<positive-set>   ::=  "[" <set-items> "]"
<negative-set>   ::=  "[^" <set-items> "]"
<set-items>      ::=  <set-item> | <set-item> <set-items>
<set-item>       ::=  <range> | <char>
<range>          ::=  <char> "-" <char>

真的没原来以为得那么神秘是不[3]

都学到这儿了…… 顺带再自学个东西吧

这个东西叫 glob Global 的缩写你可以把它理解为超级简化版正则表达式” —— 它最初是 Unix/Posix 操作系统中用来匹配文件名的通配符”。

先看一张 1971 Unix 操作系统中关于 glob 的截图

A screenshot of the original 1971 Unix reference page for glob – note the owner is dmr, short for Dennis Ritchie.

glob 的主要符号只有这么几个

  • *
  • ?
  • [abc]
  • [^abc]

现在的你打开 Wikipedia 上的关于 glob Wildcard character 的页面肯定能做到无障碍理解

顺带说现在你再去读关于 Format String 的官方文档就不会再觉得根本看不懂恰恰相反你会觉得我怎么之前连这个都看不懂呢?”

https://docs.python.org/3/library/string.html#format-string-syntax

在自学这件事上失败者的死法看起来千变万化但其实都是一样的…… 只不过是因为怕麻烦或者基础知识不够而不去读最重要的文档

比如学英语的时候死活不读语法书其实英文语法书也没多难啊再厚不也是用来的吗不就是多记几个标记就可以读懂的吗比如词性标记v., n., adj., adv., prep.... 不就是相当于地图上的图例吗那语法书和现在这里提到的官方文档不都是自学者地图

但就是这么一点点简单的东西挡住了几乎所有人真是可怕


脚注

[1]The Python Language Reference » 1.2 Notation —— 这个链接必须去看一看……

↑Back to Content↑

[2]Perl Style Regular Expressions in Prolog CMPT 384 Lecture Notes Robert D. Cameron November 29 - December 1, 1999

↑Back to Content↑

[3]很少有人注意到在很多编程语言的文法文档中"$" 被称为 <eos> —— 2017 5 月我投资了一个初创公司听说他们的资产名称叫做 eos…… 我当场就被这个梗逗乐了

↑Back to Content↑