Python 官方文档里有一块容易被忽略的宝藏:语言参考中的 grammar 规则。它们用 BNF(Backus-Naur Form)记法写成,看起来像枯燥的形式化定义,但一旦你学会读它,就能精确理解每一个语法构造的边界——哪些写法合法、哪些不合法,以及为什么。这篇文章带你拆解 BNF 记法,并用它回答几个日常编程中容易踩坑的语法问题。
BNF 记法速览
BNF 是一种描述语言语法的元语言。Python 文档用的是稍微扩展的版本,核心规则只有几条:
| 符号 | 含义 | 例子 |
|---|---|---|
name ::= ... |
定义一个语法规则(非终结符) | stmt ::= simple_stmt \| compound_stmt |
"keyword" |
字面量关键字,必须原样出现 | "if" |
| |
多选一(或) | "+" \| "-" |
[ item ] |
可选,出现 0 或 1 次 | [ "else" ":" suite ] |
( item ) |
分组 | ("a" \| "b") |
{ item } |
重复 0 或多次 | { "," expr } |
ITEM+ |
等价于 ITEM { ITEM },至少出现 1 次 |
parameter+ |
一个关键区分:大写单词(如 NAME、NUMBER)是终结符,对应词法层面的 token;小写单词(如 expr、stmt)是非终结符,会继续展开为更细的规则。
从官方文档里找规则
打开 Python Language Reference,你会看到完整的 grammar 文件。更方便的做法是在文档的各章节里就地阅读——比如"Compound statements"章节就直接内嵌了 if_stmt、while_stmt 等规则。
让我们看一个真实例子——if 语句的完整定义:
if_stmt ::= "if" assignment_expr ":" suite
("elif" assignment_expr ":" suite)*
["else" ":" suite]
逐行翻译:
- 必须以
"if"开头,后跟一个assignment_expr(即海象运算符也合法的条件表达式),再跟":"和一个suite(语句块)。 - 可以有零或多个
"elif"分支——注意是*,不是+,所以没有elif也合法。 "else"分支可选——[...]表示 0 或 1 次。
这就精确回答了一个常见疑问:elif 和 else 的顺序能不能颠倒? 不能。BNF 里 "elif" 在 "else" 前面,且 "else" 只能出现一次。想写 else ... elif?语法层面直接拒绝。
用 grammar 解答三个日常疑问
空语句合法吗?
看 suite 的定义:
suite ::= simple_stmt | NEWLINE INDENT stmt+ DEDENT
单行 suite 是 simple_stmt,而 simple_stmt 可以是 pass_stmt。多行 suite 要求至少一个 stmt(stmt+),但 stmt 包含 simple_stmt,而 simple_stmt 包含 pass。所以:
# 合法:suite 是单行 simple_stmt,pass 是合法的 simple_stmt
if condition:
pass
# 也合法:stmt+ 里只有一个 pass
if condition:
pass
但如果你在多行块里什么都不写,只有注释?注释不是 stmt,所以纯注释块会报 IndentationError——这不是风格问题,是 grammar 硬性规定。
海象运算符能在 if 条件里用吗?
回到 if_stmt:条件位置是 assignment_expr,不是 expression。assignment_expr 的定义:
assignment_expr ::= or_expr ["=" or_expr]
| or_expr ":=" or_expr
所以 := 在 if 条件里完全合法,这是 grammar 直接支持的:
if (match := pattern.search(text)):
print(match.group(0))
函数参数的星号和解包顺序
看 parameters 的定义:
parameters ::= "(" [parameter_list] ")"
parameter_list ::= defparameter ("," defparameter)*
| "*" parameter ("," defparameter)* ["," "**" parameter]
| "**" parameter
这直接告诉你:
*args之后只能跟普通参数或**kwargs,不能再出现无星号参数。**kwargs只能出现在末尾。*和**不能同时出现在*之前的位置。
所以 def f(a, *args, b, **kwargs) 合法(b 是 defparameter,出现在 *args 之后),但 def f(a, **kwargs, b) 不合法。
实践:用 Python 的 ast 模块验证语法边界
读 BNF 是理论,动手验证是实践。Python 的 ast 模块可以帮你快速判断一段代码是否在语法层面合法——不执行,只解析:
import ast, sys
def is_syntax_valid(source: str) -> bool:
"""检查源代码是否符合 Python 语法规则(不执行)。"""
try:
ast.parse(source)
return True
except SyntaxError as e:
print(f" 语法错误: {e.msg} (行 {e.lineno})")
return False
# 验证几个有争议的写法
tests = {
"elif 在 else 之后": """
if x:
pass
else:
pass
elif y:
pass
""",
"海象运算符在 if 条件": """
if (n := len(data)) > 10:
print(n)
""",
"纯注释块": """
if True:
# 只有注释
""",
"星号参数顺序错误": """
def f(a, **kwargs, b):
pass
""",
}
for desc, code in tests.items():
print(f"\n【{desc}】")
result = is_syntax_valid(code.strip())
print(f" 结果: {'合法 ✓' if result else '不合法 ✗'}")
运行输出大致如下:
【elif 在 else 之后】
语法错误: invalid syntax (行 5)
结果: 不合法 ✗
【海象运算符在 if 条件】
结果: 合法 ✓
【纯注释块】
语法错误: expected an indented block (行 2)
结果: 不合法 ✗
【星号参数顺序错误】
语法错误: invalid syntax (行 1)
结果: 不合法 ✗
ast.parse 只做语法检查,不执行代码,非常适合在 CI 或教学场景中做静态验证。你可以把 tests 字典扩展成自己的语法边界测试集。
更进一步:直接读取 Python 的 grammar 文件
Python 源码里有一份完整的 grammar 定义,你可以用脚本提取并搜索特定规则:
# 下载 Python 3.12 的 Grammar 文件
curl -sL https://raw.githubusercontent.com/python/cpython/v3.12.0/Grammar/python.gram \
| grep -A5 "if_stmt"
输出会包含 PEG 格式的 if_stmt 规则(Python 3.9 起从 PEG 解析器切换,grammar 文件也从纯 BNF 变成了 PEG 记法,但语义基本一致)。如果你想看传统 BNF 格式,文档各章节内嵌的规则仍然是权威参考。
读懂 grammar 的实际收益
| 场景 | 不读 grammar 的做法 | 读 grammar 的做法 |
|---|---|---|
| 判断某种参数顺序是否合法 | 试运行,靠报错猜 | 直接查 parameter_list 规则 |
理解 match 语句的结构 |
看教程示例 | 查 match_stmt 的完整 BNF |
| 写 linter 或代码生成器 | 靠经验补规则 | 以 grammar 为唯一权威来源 |
| 解释"为什么这样写不合法" | "Python 不允许" | "BNF 规则要求 **kwargs 只能在末尾" |
采纳建议与注意事项
- 从文档章节入手,别直接啃整个 grammar 文件。 文档按主题组织,每个语法构造旁边就有对应的 BNF,上下文更清晰。
- 注意 PEG 和传统 BNF 的差异。 Python 3.9+ 的内部 grammar 是 PEG 格式(
python.gram),文档里展示的仍是传统 BNF 风格。两者语义几乎一致,但 PEG 不依赖缩进 token(INDENT/DEDENT),这些在文档 BNF 里仍然出现。 - BNF 只管语法,不管语义。
ast.parse通过的代码,运行时仍可能报错(如类型错误、未定义变量)。grammar 是第一道门,不是最后一道。 - 遇到模糊点,用
ast.parse验证。 读 BNF 理解规则,写代码验证边界,两者配合最可靠。
下次遇到"Python 到底允不允许这样写"的问题,别只搜 Stack Overflow——打开语言参考,查对应的 BNF 规则,答案往往比任何回答都精确。