BNF 表示法:深入研究 Python 语法
在阅读 Python 文档时,您可能会发现 BNF 表示法(巴科斯-诺尔形式)的片段,如下所示:
name ::= lc_letter (lc_letter | "_")*
lc_letter ::= "a"..."z"
这些奇怪的代码是什么意思?这如何帮助您理解 Python 概念?你如何阅读和解释这个符号?
在本教程中,您将了解 Python 的 BNF 表示法的基础知识,并学习如何利用它来深入了解该语言的语法和文法。
在本教程中,您将:
- 了解 BNF 表示法是什么及其用途
- 探究Python的BNF变体的特点
- 了解如何阅读Python 文档中的 BNF 表示法
- 探索一些阅读 Python BNF 表示法的最佳实践
为了充分利用本教程,您应该熟悉 Python 语法,包括关键字、运算符以及一些常见的结构,例如表达式、条件语句和循环。
了解巴科斯-诺尔范式表示法 (BNF)
巴科斯-诺尔形式或巴科斯范式 (BNF) 是上下文无关语法的元语法表示法。计算机科学家经常使用这种表示法来描述编程语言的语法,因为它允许他们编写语言语法的详细描述。
BNF 表示法由三个核心部分组成:
组件:端子
描述:字符串必须与输入中的特定项目完全匹配。
示例:
"def"
"return"
":"
组件:非终结符
描述:将被具体值替换的符号。它们也可以简称为语法变量。
示例:
<letter>
<digit>
组件:规则
描述:定义这些元素如何关联的终结符和非终结符的约定。
示例:
<letter> ::= "a"
通过组合终结符和非终结符,您可以创建 BNF 规则,其详细程度可根据您的需要而定。非终结符必须有自己的定义规则。在一段语法中,您将有一个根规则和可能的许多定义所需非终结符的辅助规则。这样,您最终可能会得到规则的层次结构。
BNF 规则是 BNF 语法的核心组成部分。因此,语法是一组 BNF 规则,也称为产生式规则。
在实践中,您可以构建一组 BNF 规则来指定语言的语法。这里,语言是指根据相应语法中定义的规则有效的一组字符串。 BNF主要用于编程语言。
例如,Python 语法有一个定义为一组 BNF 规则的语法,这些规则用于验证任何 Python 代码片段的语法。如果代码不满足规则,那么您将收到 SyntaxError
。
您会发现原始 BNF 表示法的许多变体。一些最相关的包括扩展巴克斯-诺尔形式(EBNF)和增强巴克斯-诺尔形式(ABNF)。
在以下部分中,您将学习创建 BNF 规则的基础知识。请注意,您将使用符合 BNF Playground 站点要求的 BNF 变体,您将用它来测试您的规则。
BNF 规则及其组成部分
正如您已经了解到的,通过组合终结符和非终结符,您可以创建 BNF 规则。这些规则通常遵循以下语法:
<symbol> ::= expression
在 BNF 规则语法中,有以下部分:
<symbol>
是非终结符变量,通常用尖括号 (<>
) 括起来。::=
表示左侧的非终结符将被右侧的表达式替换。表达式
由一系列终结符、非终结符和其他定义特定语法的符号组成。
在构建 BNF 规则时,可以使用各种具有特定含义的符号。例如,如果您要使用 BNF Playground 站点来编译和测试您的规则,那么您会发现自己使用了以下一些符号:
""
包含一个终端符号
<>
表示非终结符号
()
-
表示一组有效选项
+
指定一个或多个前一元素
*
指定零个或多个前一个元素
?
指定前一个元素出现零次或一次
|
表示您可以选择其中一个选项
[x-z]
表示字母或数字间隔
一旦您知道如何编写 BNF 规则以及使用哪些符号,您就可以开始创建自己的规则。请注意,BNF Playground 有几个额外的符号和语法结构,您可以在规则中使用它们。如需完整参考,请单击页面顶部的语法帮助部分。
现在,是时候开始使用一些自定义 BNF 规则了。首先,您将从一个通用示例开始。
通用示例:全名语法
假设您需要创建一个上下文无关语法来定义用户应如何输入一个人的全名。在这种情况下,全名将由三个部分组成:
- 名
- 中间名字
- 姓
在每个组件之间,您需要恰好放置一个空格。您还应该将中间名视为可选。以下是定义此规则的方法:
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name>
BNF 规则的左侧部分是一个非终结符变量,用于标识该人的全名。 ::=
符号表示
将替换为规则的右侧部分。
规则的右侧部分有几个组成部分。首先,您有名字,您可以使用
非终结符定义它。接下来,您需要一个空格来分隔名字和以下部分。要定义此空格,您可以使用终端,它由引号之间的空格字符组成。
在名字之后,您可以接受中间名,之后,您需要另一个空格。因此,您可以打开括号来对这两个元素进行分组。然后创建
和 " "
终端。两者都是可选的,因此您可以在后面使用问号 (?
) 来表示该条件。
最后,您需要姓氏。要定义此组件,您可以使用另一个非终结符
。就是这样!您已经构建了第一个 BNF 规则。然而,你仍然没有一个有效的语法。你只有一条根规则。
要完成语法,您需要为
、
和
定义规则。为此,您需要满足一些要求:
- 每个名称组件仅接受字母。
- 每个名称组成部分都以大写字母开头,并以小写字母继续。
在这种情况下,您可以首先定义两个规则,一个用于大写字母,一个用于小写字母:
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name>
<uppercase_letter> ::= [A-Z]
<lowercase_letter> ::= [a-z]
在此语法片段的突出显示行中,您创建了两个非常相似的规则。第一条规则接受从大写 A 到 Z 的所有 ASCII 字母。第二条规则接受所有小写字母。在此示例中,您不支持重音符号或其他非 ASCII 字母。
有了这些规则,您就可以构建其余的规则。首先,请继续添加
规则:
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name>
<uppercase_letter> ::= [A-Z]
<lowercase_letter> ::= [a-z]
<first_name> ::= <uppercase_letter> <lowercase_letter>*
要定义
规则,请以
非终结符开头,以表示名称的第一个字母必须是大写字母。然后,继续使用
非终结符后跟星号 (*
)。该星号表示名字将在初始大写字母之后接受零个或多个小写字母。
您可以遵循相同的模式来构建
和
规则。您想尝试一下吗?完成后,单击下面的可折叠部分获取完整的语法,以便您可以将其与您的进行比较:
<full_name> ::= <first_name> " " (<middle_name> " ")? <family_name>
<uppercase_letter> ::= [A-Z]
<lowercase_letter> ::= [a-z]
<first_name> ::= <uppercase_letter> <lowercase_letter>*
<middle_name> ::= <uppercase_letter> <lowercase_letter>*
<family_name> ::= <uppercase_letter> <lowercase_letter>*
您可以使用 BNF Playground 网站检查您的全名语法是否有效。这是一个演示:
导航到 BNF Playground 站点后,您可以将语法规则粘贴到文本输入区域中。然后按编译 BNF 按钮。如果您的 BNF 规则一切正常,那么您可以在在此处测试字符串! 输入字段中输入全名。输入某人的全名后,如果输入字符串符合规则,该字段将变成绿色。
与编程相关的示例:标识符
在上一节中,您学习了如何创建 BNF 语法来定义用户必须如何提供人名。这是一个通用示例,可能与编程相关,也可能无关。在本节中,您将通过编写一组简短的 BNF 规则来验证假设的编程语言中的标识符来获得更多技术知识。
标识符可以是变量、函数、类或对象的名称。在您的示例中,您将编写一组规则来检查给定字符串是否满足以下要求:
- 第一个字符是大写或小写字母或下划线。
- 其余字符可以是大写或小写字母、数字或下划线。
这是标识符的根规则:
<identifier> ::= <char> (<char> | <digit>)*
在此规则中,您有 <identifier>
非终结符变量,它定义根。在右侧,首先有 <char>
非终结符。标识符的其余部分分组在括号内。组后面的星号表示该组中的元素可以出现零次或多次。每个这样的元素要么是一个字符,要么是一个数字。
现在,您需要使用自己的专用规则定义 <char>
和 <digit>
非终结符。它们看起来像下面的代码:
<identifier> ::= <char> (<char> | <digit>)*
<char> ::= [A-Z] | [a-z] | "_"
<digit> ::= [0-9]
<char>
规则接受一个小写或大写的 ASCII 字母。或者,它可以接受下划线。最后,<digit>
规则接受 0 到 9 之间的数字。现在,您的规则集已完成。请前往 BNF Playground 网站尝试一下。
对于程序员来说,阅读 BNF 规则可能是一项非常有用的技能。例如,您经常会发现许多编程语言的官方文档全部或部分包含这些语言的 BNF 语法。因此,能够阅读 BNF 可以让您更好地理解语言语法和复杂性。
从现在开始,您将学习如何阅读 Python 的 BNF 变体,您可以在语言文档的多个部分中找到它。
理解 Python 的 BNF 变体
Python 使用 BNF 表示法的自定义变体来定义语言的语法。在 Python 文档的许多部分中,您都会找到 BNF 语法的部分内容。这些片段可以帮助您更好地理解您正在学习的任何语法结构。
Python 的 BNF 变体使用以下样式:
name
保存规则或非终结符的名称
::=
意味着扩展为
|
分隔替代品
*
接受前一项的零次或多次重复
+
接受前一项的一次或多次重复
[]
接受零次或一次出现,这意味着随附的项目是可选的
()
组选项
""
定义文字字符串
- space
仅对分隔token有意义
这些符号定义了 Python 的 BNF 变体。与常规 BNF 规则的一个显着区别是 Python 不使用尖括号 (<>
) 来括起非终结符号。它仅使用非终结符标识符或名称。可以说,这使得规则更清晰、更具可读性。
另请注意,方括号 ([]
) 对于 Python 具有不同的含义。到目前为止,您已经使用它们来包围 [a-z]
等字符集。在 Python 中,这些方括号意味着包含的元素是可选的。要在 Python 的 BNF 变体中定义类似 [a-z]
的内容,您将使用 "a"..."z"
代替。
您会在 Python 文档中找到许多 BNF 片段。作为一名 Python 开发人员,学习如何浏览和阅读它们是一项非常有用的技能。因此,在接下来的部分中,您将探索 Python 文档中的一些 BNF 规则示例,并学习如何阅读它们。
从 Python 文档中读取 BNF 规则:示例
现在您已经了解了阅读 BNF 表示法的基础知识,并且已经了解了 Python 的 BNF 变体的特征,现在是时候开始从 Python 文档中阅读一些 BNF 语法了。通过这种方式,您将掌握所需的技能,以利用此表示法来了解有关 Python 及其语法的更多信息。
pass
和 return
语句
首先,您将从 pass
语句开始,这是一个简单的语句,允许您在 Python 中执行任何操作。该语句的 BNF 表示法如下:
pass_stmt ::= "pass"
在这里,您有规则的名称 pass_stmt
。然后,您可以使用 ::=
符号来指示规则扩展为 "pass"
,这是一个终端符号。这意味着该语句由 pass
关键字组成。没有额外的语法成分。因此,您最终了解了 pass
语句的语法:
pass
pass
语句的 BNF 规则是文档中最简单的规则之一。它只包含一个直接定义语法的终端。
在日常编码中经常使用的另一个常见语句是return
。该语句比 pass
稍微复杂一些。以下是文档中 return
的 BNF 规则:
return_stmt ::= "return" [expression_list]
在本例中,您将像平常一样拥有规则的名称 return_stmt
和 ::=
。然后,您将得到一个由单词 return
组成的终端符号。该规则的第二个组成部分是可选的表达式列表,expression_list
。您知道第二个组件是可选的,因为它括在方括号中。
在单词 return
之后有一个可选的表达式列表与 Python 允许没有显式返回值的 return
语句的事实是一致的。在这种情况下,语言自动返回 None
,这是 Python 的 null 值:
>>> def func():
... return
...
>>> print(func())
None
这个玩具函数使用一个简单的返回
,而不提供显式的返回值。在这种情况下,Python 会自动为您返回 None
。
现在,如果您单击文档上的 expression_list
变量,那么您将看到以下规则:
expression_list ::= expression ("," expression)* [","]
同样,您有规则的名称和 ::=
符号。然后,您有一个必需的非终结符变量,表达式
。该非终结符号有其自己的定义规则,您可以通过单击符号本身来访问该规则。
到目前为止,您已经有了带有单个返回值的 return
语句的语法:
>>> def func():
... return "Hello!"
...
>>> func()
'Hello!'
在此示例中,您使用 "Hello!"
字符串作为函数的返回值。请注意,返回值可以是任何 Python 对象或表达式。
该规则通过左括号继续。请记住,BNF 使用括号对对象进行分组。在本例中,您有一个由逗号 (","
) 组成的终端,然后您再次有 表达式
符号。右括号后的星号表示该构造可以出现零次或多次。
规则的这一部分描述了具有多个返回值的返回语句:
>>> def func():
... return "Hello!", "Pythonista!"
...
>>> func()
('Hello!', 'Pythonista!')
现在,您的函数返回两个值。为此,您需要提供一系列以逗号分隔的值。当您调用该函数时,您会得到一个值的元组。
规则的最后部分是[","]
。这告诉您表达式列表可以包含可选的尾随逗号。这个逗号可能会导致棘手的结果:
>>> def func():
... return "Hello!",
...
>>> func()
('Hello!',)
在此示例中,您在单个返回值后使用尾随逗号。结果,您的函数返回一个包含单个项目的元组。但是,请注意,如果您已经有多个逗号分隔值,则逗号不会产生任何影响:
>>> def func():
... return "Hello!", "Pythonista!",
...
>>> func()
('Hello!', 'Pythonista!')
在此示例中,您将尾随逗号添加到具有多个返回值的 return 语句中。同样,当您调用该函数时,您会得到一个值的元组。
赋值表达式
您可以在 Python 文档中找到另一个有趣的 BNF 片段,它定义了赋值表达式的语法,您可以使用 walrus 运算符构建该表达式。以下是此类表达式的根 BNF 规则:
assignment_expression ::= [identifier ":="] expression
该规则的右侧部分以一个可选组件开始,其中包括一个名为 identifier
的非终结符和一个由 ":="
符号组成的终结符。该符号是海象运算符本身。然后,你就有了一个所需的表达式。
注意:乍一看,赋值部分是可选的可能会很奇怪,因为赋值表达式的重点就是赋值本身。然而,使这部分成为可选的大大简化了许多语法规则,因为几乎在任何有纯表达式的地方都允许赋值表达式。您将在下一节中看到这种简化的示例。
这与带有海象运算符的赋值表达式的语法相匹配:
identifier := expression
请注意,在赋值表达式中,赋值部分是可选的。无论是否执行赋值,您都会从表达式求值中获得相同的值。
这是赋值表达式的一个工作示例:
>>> (length := len([1, 2, 3]))
3
>>> length
3
在此示例中,您将创建一个赋值表达式,将列表中的项目数赋给 length
变量。
请注意,您已将表达式括在括号中。否则,它将失败并出现 SyntaxError
异常。查看《海象运算符:Python 3.8 赋值表达式》中的海象运算符语法部分,了解为什么需要括号。
条件语句
现在您已经学会了如何阅读简单表达式的 BNF 规则,您可以跳转到复合语句。条件语句在任何 Python 代码中都很常见。 Python 文档为此类语句提供了 BNF 规则:
if_stmt ::= "if" assignment_expression ":" suite
("elif" assignment_expression ":" suite)*
["else" ":" suite]
当您开始阅读此规则时,您会立即找到 "if"
终结符,您必须使用它来启动任何条件语句。然后,您会找到 赋值表达式
非终结符,您已在上一节中学习过它。
注意:if_stmt
规则使用assignment_expression
非终结符来定义条件。这允许您在条件中使用赋值表达式或普通表达式。请记住,赋值部分在赋值表达式
中是可选的。
接下来,您有 ":"
终端。这是您需要在复合语句标题末尾使用的冒号。这个冒号表示语句的标题是完整的。最后,您有一个名为 suite
的必需非终结符,它是一组缩进语句。
遵循规则的第一部分,您最终会得到以下 Python 语法:
if assignment_expression:
suite
这是一个简单的 if
语句。它以 if
关键字开头。然后,您就得到了 Python 计算真值的表达式。最后,你有一个冒号,它可以让一个缩进块充当套件。
BNF 规则的第二行定义了 elif 子句的语法。在此行中,您使用 elif
关键字作为终端符号。然后,你有一个表达式、一个冒号,以及一套缩进代码:
if assignment_expression:
suite
elif assignment_expression:
suite
条件语句中可以有零个或多个 elif 子句,您可以通过右括号后面的星号知道这些子句。它们都遵循相同的语法。
条件 BNF 规则的最后部分是 else
子句,它由 else
关键字后跟一个冒号和缩进的代码套件组成。以下是将其转换为 Python 语法的方式:
if assignment_expression:
suite
elif assignment_expression:
suite
else:
suite
在 Python 中,else
子句也是可选的。在 BNF 规则中,您可以通过包围规则最后一行的方括号知道这一点。
这是一个工作条件语句的玩具示例:
>>> def read_temperature():
... return 25
...
>>> if (temperature := read_temperature()) < 10:
... print("The weather is cold!")
... elif 10 <= temperature <= 25:
... print("The weather is nice!")
... else:
... print("The weather is hot!")
...
The weather is nice!
在 if
子句中,您使用赋值表达式来获取当前温度值。然后,将当前值与 10
进行比较。接下来,您将重用温度值来创建 elif
子句中的表达式。最后,对于温度较高的情况,您可以使用 else
子句。
循环结构
循环是 Python 中另一种常用的复合语句。 Python 中有两个不同的循环语句:
for
while
Python for
循环的 BNF 语法如下:
for_stmt ::= "for" target_list "in" starred_list ":" suite
["else" ":" suite]
第一行定义循环头,它以 "for"
终端开始。然后你就有了 target_list
非终结符。简而言之,这个非终结符代表一个或多个循环变量。
接下来,您有 "in"
终端,它代表 in
关键字。 starred_list
非终结符表示一个可迭代对象。最后,您有一个冒号,它传递给缩进的代码块 suite
。
注意:Python 的语法在不断演变。例如,在Python 3.10中,for
循环规则写为:
for_stmt ::= "for" target_list "in" expression_list ":" suite
["else" ":" suite]
此处,您使用的是 expression_list
,而不是 starred_list
。在 Python 3.11 中,加星号的列表在 for
循环中变得有效。所以,语法发生了变化。
同样,您可以单击任何非终结符来导航到其定义的 BNF 规则,并更深入地了解其定义和语法。例如,如果您单击 target_list
符号,那么您将看到以下 BNF 规则:
target_list ::= target ("," target)* [","]
target ::= identifier
| "(" [target_list] ")"
| "[" [target_list] "]"
| attributeref
| subscription
| slicing
| "*" target
在第一行中,您可以看到 target_list
由一个或多个以逗号分隔的 target
对象组成。此列表可以包含可选的尾随逗号,这不会改变结果。实际上,目标对象可以是标识符(变量)、元组、列表或任何其他提供的选项。管道字符 (|
) 让您知道所有这些值都是单独的替代值。
for
循环的 BNF 规则的第二行定义了循环的 else
子句的语法。该子句是可选的,您可以从方括号中了解到这一点。该行由 "else"
终端组成,后跟一个冒号和一组缩进代码。
您可以将上述 BNF 规则转换为以下 Python 语法:
for target_list in starred_list:
suite
else:
suite
该循环在 target_list
中具有一系列以逗号分隔的循环变量,以及由 starred_list
表示的可迭代数据。
下面是一个 for
循环的简单示例:
>>> high = 5
>>> for number in range(high):
... if number > 5:
... break
... print(number)
... else:
... print("range covered")
...
0
1
2
3
4
range covered
此循环迭代从 0
到 high
的一系列数字。在此示例中,high
为 5
,因此 break
语句不会运行,而 else
子句会运行在循环的末尾。如果将 high
的值更改为 10
,则 break
语句将运行,并且 else
子句获胜't。
注意:如果循环的主套件没有 break
语句,那么带有 else
子句的循环就没有意义。如果您发现自己处于这种情况,请删除 else:
标头并取消缩进其套件。
当谈到 while
循环时,它们的 BNF 规则如下:
while_stmt ::= "while" assignment_expression ":" suite
["else" ":" suite]
Python 的 while 循环以 while
关键字开始,它是规则右侧部分的第一个组成部分。然后,您需要一个赋值表达式
、一个冒号和一组缩进代码:
while assignment_expression:
suite
else:
suite
请注意,while
循环还有一个可选的 else
子句,其工作方式与 for
循环中的相同。您能提供一个 while 循环的工作示例吗?
探索阅读 Python BNF 的最佳实践
当您在文档中阅读 Python 的 BNF 规则时,您可以遵循一些最佳实践来增进您的理解。以下是给您的一些建议:
- 熟悉 BNF 表示法:熟悉其基本概念和语法。了解非终结符、终结符、产生式规则等术语。
- 实验和实践:编写小型自定义 BNF 规则并使用 BNF Playground 网站进行实验。
- 熟悉 Python 的 BNF 变体:了解 Python 用于定义其 BNF 变体的符号。了解分组、表达重复和可选性的符号是一项必备技能。
- 分解 BNF 规则:将 BNF 规则分解为更小的部分,并单独分析每个组件。
- 识别非终结符号:在 BNF 规则中查找非终结符号。这些符号包含链接,您可以单击这些链接来导航到其定义。
- 识别终结符:查找代表语言中特定元素的终结符,例如关键字、运算符、文字或标识符。这些符号用引号引起来。
- 研究示例:研究与您想要理解的 BNF 规则相对应的实际示例。分析 BNF 规则如何应用于这些示例。将该规则与实际的 Python 语法进行对比。
- 查看其他注释或解释:阅读您正在学习的 BNF 规则的文档中提供的其他注释。
如果你将这些建议应用到你的 BNF 阅读冒险中,那么你会感觉更舒服。在此过程中,您将能够更好地理解规则并提高您的 Python 技能。
结论
现在您知道什么是 BNF 表示法以及 Python 在官方文档中如何使用它。您已经学习了 Python 版本的 BNF 表示法的基础知识以及如何阅读它。这是一项相当高级的技能,可以帮助您更好地理解该语言的语法和语法。
在本教程中,您已经:
- 了解什么是 BNF 表示法及其用途
- 了解Python 的 BNF 变体
- 阅读 Python 文档中 BNF 语法的一些实际示例
- 确定了一些阅读 Python 的 BNF 变体的最佳实践
了解如何阅读Python文档中的BNF表示法将使您对Python的语法和文法有更好、更深入的理解。大胆试试吧!