Python 优先顺序:某些 not 和 + 表达式中的语法错误



在Python中,为什么

3 + not 2

导致语法错误,而

not 3 + 2

解析正常吗?我在 https://docs.python.org/3/reference/expressions.html#operator-precedence 看到not低于+,但对我来说,这并不能真正解释为什么它不能解析3 + not 2

此问题是通过调查具有二进制和一元运算符、保留字且不带括号的 Parse 表达式来触发的

我相信这是在Python开发早期做出的选择。允许3 + not 2像你期望的那样被解析没有理论上的障碍,但Python碰巧不允许这样做。

与这里似乎普遍认为的相反,它与运算符优先级算法无关。尽管使用运算符优先级(略微不完全)作为文档辅助,但实际的 Python 算法使用上下文无关语法,并且使用的上下文无关语法明确指定由not运算符领导的表达式不能用作算术运算的操作数或比较。因此,像- not FalseTrue == not False这样简单的表达式也会产生语法错误。

这是语法的摘录,来自 Python 参考手册。此语法用于生成 Python 解析器,因此它是权威的,即使它不能作为文档完全可读。为了使模式更加明显,我将作品间隔开:(我也省略了很多作品,并非所有作品都完全无关紧要。如果您寻求更多详细信息,请查阅原文。

not_test: 'not' not_test | comparison
comparison: expr       ( comp_op                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]

从该语法中可以看出,以comparison开头的作品都不能包含not_expression。这就是定义not相对于比较运算符的优先级(因此not a == b等效于not (a == b))。但这不仅可以防止not a被误解析为==运算符;它还可以防止not a被用作==的右手运算符,这就是为什么True == not False是一个语法错误而不是重言式。

由于该摘录中的所有其他运算符比比较运算符绑定得更紧密,因此它们都与not具有相同的关系。

某些语言(如从 C 派生的语言)不会以这种方式降低not的优先级。在 C 语言中,布尔否定运算符!与任何其他一元运算符具有相同的优先级,因此它比比较运算符(或算术运算符)绑定得更紧密。因此,在C中,! a < b确实意味着(!a) < b,即使这可能没有那么有用。但是许多其他语言,包括大多数SQL方言,在优先级图表中NOT与Python中的优先级相同:在二进制布尔运算符和比较运算符之间。[注2]此外,大多数 SQL 方言确实允许使用NOT作为比较运算符的操作数。尽管如此,所有这些语法都基于相同的上下文无关形式。[注3]

那么他们是怎么做到的呢?编写一个明确的上下文无关语法,即使在比较和算术语法的操作数中,也可以将not用作一元运算符是一项具有挑战性的工作,并且阅读该语法也并非易事。但是,一种非常古老的解析技术使得创建解析器几乎是微不足道的。这种技术被称为"运算符优先级",你几乎可以在每个现代解析框架中找到它。然而,正如我们将看到的,它经常被误解,并且并不总是很好地实施。

例如,这里有一个简单的 AST 构建器,在 Sly 的帮助下编写。这是一个文件,但为了可读性,我将其分成三个代码块。首先是词法分析器,这在这里并不真正相关:

from sly import Lexer, Parser
class CalcLexer(Lexer):
tokens = { NAME, NUMBER, POW, LE, GE, NE, AND, OR, NOT, TRUE, FALSE }
ignore = ' t'
literals = { '=', '<', '>', '+', '-', '*', '/', '(', ')' }
# Multicharacter symbol tokens
LE = r'<='
GE = r'>='
NE = r'!= | <>'
POW = r'**'
# Keyword tokens (and identifiers)
NAME = r'[a-zA-Z_][a-zA-Z0-9_]*'
NAME['and']   = AND
NAME['false'] = FALSE
NAME['not']   = NOT
NAME['or']    = OR
NAME['true']  = TRUE
@_(r'd+')
def NUMBER(self, t):
t.value = int(t.value)
return t
@_(r'n+')
def newline(self, t):
self.lineno += t.value.count('n')
def error(self, t):
print("Illegal character '%s'" % t.value[0])
self.index += 1

现在,解析器。请注意靠近顶部的优先级定义:

class CalcParser(Parser):
tokens = CalcLexer.tokens
precedence = (
('left', OR),
('left', AND),
('right', NOT),
('left', '=', NE),
('left', '<', LE, GE, '>'),
('left', '+', '-'),
('left', '*', '/'),
('right', UMINUS),
('right', POW),
)
@_('expr')
def statement(self, p):
print(p.expr)
@_('expr OR expr')
@_('expr AND expr')
@_('expr "=" expr')
@_('expr NE expr')
@_('expr "<" expr')
@_('expr LE expr')
@_('expr GE expr')
@_('expr ">" expr')
@_('expr "+" expr')
@_('expr "-" expr')
@_('expr "*" expr')
@_('expr "/" expr')
@_('expr POW expr')
def expr(self, p):
return [p[1], p.expr0, p.expr1]
@_('"-" expr %prec UMINUS')
@_('NOT expr')
def expr(self, p):
return [p[0], p.expr]
@_('"(" expr ")"')
def expr(self, p):
return p.expr
@_('NUMBER')
@_('NAME')
@_('TRUE')
@_('FALSE')
def expr(self, p):
return p[0]

最后,一个简单的驱动程序:

if __name__ == '__main__':
try:
import readline
except:
pass
lexer = CalcLexer()
parser = CalcParser()
while True:
try:
text = input('calc > ')
except EOFError:
break
if text:
parser.parse(lexer.tokenize(text))

还有一个快速测试,表明它具有(希望)预期的行为:

$ python3 calc.py
calc > 3+4
['+', 3, 4]
calc > 3+not 4
['+', 3, ['not', 4]]
calc > not 3 + 4
['not', ['+', 3, 4]]
calc > not 3*4<7
['not', ['<', ['*', 3, 4], 7]]
calc > not 3*4<7 and not true
['and', ['not', ['<', ['*', 3, 4], 7]], ['not', 'true']]

在 LR 解析变得实用之前(使用 Frank deRemer 的高效算法构造 LALR(1) 解析器),通常使用所谓的"运算符优先"解析器,这在 1963 年由 Robert W. Floyd 在一篇论文《句法分析和运算符优先级》中首次描述 [注释 4]。运算符优先级解析器是 [shift-reduce 解析器],其移位和归约操作基于"优先级关系"矩阵执行,由解析器堆栈顶部的运算符和接下来出现在输入流中的运算符索引。矩阵中的每个条目都有以下四个可能值之一:

  • ⋖,表示输入符号应移位。
  • ⋗,表示应减少堆栈符号。
  • ≐,表示堆栈符号和前瞻符号属于同一生产。
  • ,表示语法错误。

尽管它们与比较运算符相似,但 Floyd 描述的优先关系甚至不是部分排序,因为它们不是传递的,也不是反对称的。但在大多数实用语法中,它们可以简化为数字比较,但有两个主要的警告:

  1. 原始关系矩阵包含空条目,表示非法语法。将关系简化为数值比较会为每个条目提供一个定义的值,因此生成的解析器将无法检测到某些语法错误。

  2. 由于优先级关系不是真正的比较,因此通常无法使用单个数字映射来表示它们。您需要使用两个函数,"左优先级"和"右优先级",一个用于左侧的运算符,另一个用于右侧的运算符。

从某种意义上说,这些问题中的第一个并不那么严重,因为 Floyd 的算法本身并不一定能检测到所有的语法错误。实际上,运算符优先级解析消除了不同非终端之间的差异;所有归约操作都会产生一种通用的非终端,这可能会丢失区分不同句法情况所需的信息。如果信息丢失导致无法在两种不同的归约之间做出决定,则无法使用运算符优先级分析语法。但更常见的是,信息丢失导致根本无法检测到语法错误,这可能被认为是可以接受的。

无论如何,弗洛伊德的算法变得非常流行。它被用来为许多语言构建解析器,并产生了一些仍然流行的范式,例如调车场算法。

通常,计算优先级矩阵然后将其简化为一对函数很难手动完成。但是在一个特定的领域——代数表达式——结果是如此直观,以至于它们已经取代了原来的定义。如果要分析不带括号和一元运算符的代数表达式,只需将运算符分组到优先级级别即可。每个级别分配有两个连续的整数(没有两个级别使用相同的整数),用作左优先级和右优先级值。

为了区分左关联运算符和右关联运算符,有两个整数:对于左关联级别,左优先级是两个整数中的较大者;对于右关联级别,右优先级更大。

这不是唯一的解决方案。你会发现许多其他人。例如,将其描述为带有双状态枚举(关联性)的单个整数是很常见的,如果比较的两个整数相等,则考虑关联性。这种"简化"通常不适用于运算符优先语法,因为括号和一元运算符不能简单地从语言中挥手。但是,如果在应用优先规则的上下文中,解析框架已经处理了括号,则可能是可以接受的。

使用运算符优先级分析器处理括号没有问题。当您坚持符号的左优先级和右优先级是连续整数(或单个整数和标志)时,就会出现问题:

  • (始终移动到堆栈上,这意味着其右优先级高于任何运算符的左优先级。
  • (位于堆栈顶部时,没有运算符导致它降低,这意味着它的左优先级低于任何运算符的右优先级。(它最终被匹配)减少)。

一元运算符具有类似的不对称性。前缀运算符也总是移动,因为没有前面的非终端要减少。但是,正如NOT的情况所示,运算符不一定比任何后续运算符具有更高的优先级。因此,它的左优先级和右优先级可能有很大差异。

(待续)

<小时 />

注释:

  1. 如果我们只想检查表达式的语法,我们可以消除not_testfactor中的递归生产,这可能是混淆的根源。我们可以改为写:

    not_test:              ( 'not' )* comparison
    factor:                ( '+'|'-'|'~'|atom_expr '**')* atom_expr
    

    但这不会产生正确的解析,因为它不会将运算符与其参数相关联。例如,从右到左应用幂是必不可少的。此外,我们通常不能通过以某种方式将一系列not前缀运算符合并为单个运算符来评估not

  2. 这只是表明任意运算符优先级是多么可怜。确实没有通用标准;甚至有些语言的所有运算符都具有相同的优先级,因此分组严格从右到左。

  3. 新的Python解析器实际上是一个解析表达式语法(PEG),这也是pyparsing实现的形式主义。PEG与上下文无关语法完全不同,但在这个特殊的小角落,差异并不重要。

  4. 令人愤慨的是,ACM(这篇文章首次出现在其期刊上)认为收取 15 美元是合理的,以便人们可以阅读近 60 年前发表的 18 页论文。这是付费墙的链接,以防您发现它有用。

没错:

not低于+

所以:

not 3 + 2 => not (3 + 2) => not 5 => False

因此:

3 + not 2 => (3 + not) 2 => ERROR

因为not必须采用一个参数:

>>> not
File "<stdin>", line 1
not
^
SyntaxError: invalid syntax

解析正常吗?我在 https://docs.python.org/3/reference/expressions.html#operator-precedence 看到 not 低于 +,但对我来说,这并不能真正解释为什么它不能解析 3 + 而不是 2。

意味着您尝试像 (3 + not) 2 一样计算这是错误的:"not"运算符不适合将"+"运算符作为操作数。

最新更新