用 BNF 读懂 Python 语法规则——官方文档里的隐藏地图

2026-05-28 28 预计阅读时间:1 分钟
来源:realpython.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:9 分钟

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+

一个关键区分:大写单词(如 NAMENUMBER)是终结符,对应词法层面的 token;小写单词(如 exprstmt)是非终结符,会继续展开为更细的规则。

从官方文档里找规则

打开 Python Language Reference,你会看到完整的 grammar 文件。更方便的做法是在文档的各章节里就地阅读——比如"Compound statements"章节就直接内嵌了 if_stmtwhile_stmt 等规则。

让我们看一个真实例子——if 语句的完整定义:

if_stmt ::= "if" assignment_expr ":" suite
            ("elif" assignment_expr ":" suite)*
            ["else" ":" suite]

逐行翻译:

  • 必须以 "if" 开头,后跟一个 assignment_expr(即海象运算符也合法的条件表达式),再跟 ":" 和一个 suite(语句块)。
  • 可以有零或多个 "elif" 分支——注意是 *,不是 +,所以没有 elif 也合法。
  • "else" 分支可选——[...] 表示 0 或 1 次。

这就精确回答了一个常见疑问:elifelse 的顺序能不能颠倒? 不能。BNF 里 "elif""else" 前面,且 "else" 只能出现一次。想写 else ... elif?语法层面直接拒绝。

用 grammar 解答三个日常疑问

空语句合法吗?

suite 的定义:

suite ::= simple_stmt | NEWLINE INDENT stmt+ DEDENT

单行 suitesimple_stmt,而 simple_stmt 可以是 pass_stmt。多行 suite 要求至少一个 stmtstmt+),但 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,不是 expressionassignment_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) 合法(bdefparameter,出现在 *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 规则,答案往往比任何回答都精确。


相关推荐