不同的语法错误在输出中隐藏行

Different syntax errors hide line in output

我有一个调用 compile 的脚本。

try:
    code = compile('3 = 3', 'test', 'exec')
except Exception as e:
    sys.stderr.write(''.join(traceback.format_exception_only(type(e), e)))

3 = 3 结果:

File "test", line 1
SyntaxError: can't assign to literal

3 = 3a 实际上打印了行

File "test", line 1
    3 = 3a
        ^
SyntaxError: invalid syntax

知道这是为什么吗?

Python 在两个地方产生 SyntaxError 异常:

  1. 解析时,由Python语法驱动
  2. 从解析结果创建抽象语法树 (AST) 时; AST 驱动编译器。

那是因为 Python 语法有一些特殊情况,在这些情况下,保持语法简单更容易,但随后进行额外检查 一旦解析完成 并构建进行额外语法检查的 AST。

赋值是其中之一,因为 = 符号左侧允许的规则与右侧允许的规则不同,但仍然密切相关。左侧是 target 端,目标可以像列表或元组一样构造(拆包赋值),您可以赋值给属性或索引操作(listobj[1] = ..., ETC。)。但是要让解析器检测到目标实际上是文字而不是变量名或属性等,将需要非常不同的解析器结构,所以这留给了 AST。

所以你的 3 = 3 错误通过了解析阶段,但随后在后来的 AST 'assignment target' 检查阶段失败,而 3 = 3a 落在解析器阶段(其中 3a很容易发现错误)。

为了给你一个好的语法错误,解析器引发的异常包含异常中的源代码行:

>>> try:
...     code = compile('3 = 3a', 'test', 'exec')
... except Exception as e:
...     print(repr(e))
...
SyntaxError('invalid syntax', ('test', 1, 6, '3 = 3a\n'))

注意异常中的('test', 1, 6, '3 = 3a\n')元组;这些可通过 SyntaxError 属性获得 filenamelineno(行号)、offset(列偏移)和 text 源代码行本身.对于解析器,这很容易提供,因为它可以访问源代码。

但是AST没有源代码。它只有文件名、行号、列和 parse tree objects。它没有原始源文本。它通常会尝试从文件名中读取它,但 test 实际上不是文件。所以该行是空的:

>>> try:
...     code = compile('3 = 3', 'test', 'exec')
... except Exception as e:
...     print(repr(e))
...
SyntaxError('cannot assign to literal', ('test', 1, 1, ''))

您可以对此进行测试并修复它,方法是将 SyntaxError 异常替换为新的异常,并将空字符串替换为您的源文本:

>>> source = '3 = 3'
>>> try:
...     code = compile(source, 'test', 'exec')
... except Exception as e:
...     if isinstance(e, SyntaxError) and not e.text:
...         sline = source.splitlines(True)[e.lineno - 1]
...         e = SyntaxError(e.msg, (e.filename, e.lineno, e.offset, sline))
...     sys.stderr.write(''.join(traceback.format_exception_only(type(e), e)))
...
  File "test", line 1
    3 = 3
    ^
SyntaxError: cannot assign to literal

请注意,对于多行源字符串,您希望将该源拆分为多行,并使用 .lineno 属性来 select 指定的源行。

另一种方法是将源代码写入临时文件名,并将该文件名传递给 compile(),以便在构建 AST 时发现 SyntaxError 异常时,Python然后可以打开该临时文件并找到相应的文本行。

请注意,当您使用特殊文件名 '<string>' 时,不会尝试查找某行的源代码并且 e.text 设置为 None:

>>> try:
...     code = compile('3 = 3', '<string>', 'exec')
... except Exception as e:
...     print(repr(e))
...
SyntaxError('cannot assign to literal', ('<string>', 1, 1, None))

并且当 .text 属性设置为 None 时,traceback 模块放弃打印线和标记部分。

如果您对 Python 语法解析器无法检测赋值目标中的文字的确切原因感兴趣,您可能会对 Guido van Rossum 在 writing a different parser for Python 中所做的工作感兴趣,其中包括说明当前解析器为何以这种方式工作以及替代解析器模型如何避免这些问题。