问题描述
在Python中,为什么
3 + not 2
导致SyntaxError,而
not 3 + 2
解析很好吗?我在https://docs.python.org/3/reference/expressions.html#operator-precedence看到not
比+
低,但是对我来说,这并不能真正解释为什么它不能解析3 + not 2
。
此问题是通过调查Parse expression with binary and unary operators,reserved words,and without parentheses
触发的解决方法
我相信,这是在Python开发的早期就做出的选择。从理论上讲,允许像您期望的那样解析3 + not 2
并没有障碍,但是Python恰恰不允许这样做。
与这里似乎普遍的看法相反,它与运算符优先级算法无关。尽管使用运算符优先级(略微不精确)作为文档辅助工具,但是实际的Python算法使用的是上下文无关的语法,并且使用的上下文无关的语法明确指定了以not
开头的表达式不能用作算术运算或比较的操作数。结果,像- not False
和True == not False
这样简单的表达式也会产生语法错误。
这是the Python reference manual的语法摘录。该语法用于生成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方言 do 都允许将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),'=',NE),'>'),'-'),'/'),UMINUS),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
['+',['not',4]]
calc > not 3 + 4
['not',['+',4]]
calc > not 3*4<7
['not',['<',['*',4],7]]
calc > not 3*4<7 and not true
['and',7]],'true']]
在LR解析变得可行之前(使用Frank deRemer的有效算法来构造LALR(1)解析器),通常使用所谓的“运算符优先”解析器,该解析器于1963年在Robert的论文中首次描述。 W. Floyd,《句法分析和运算符优先级》 [em] [注4]。运算符优先级解析器是[shift-reduce解析器],其移位和归约动作是根据“优先级关系”矩阵执行的,该矩阵由运算符在解析器堆栈的顶部进行索引,而运算符则出现在输入的下一部分流。矩阵中的每个条目都有四个可能的值之一:
- ⋖,表示应移动输入符号。
- ⋗,表示应减少堆栈符号。
- ≐,表示堆栈符号和超前符号属于同一生产。
- 空,表示语法错误。
尽管它们与比较运算符相似,但Floyd描述的优先级关系甚至不是部分排序,因为它们既不是可传递的,也不是反对称的。但是在大多数实用的语法中,可以将它们简化为数值比较,但有两个主要警告:
-
关系的原始矩阵包含空条目,表示非法语法。减少与数值比较的关系会为每个条目提供一个定义的值,因此生成的解析器将无法检测到某些语法错误。
-
因为优先级关系并不是真正的比较,所以通常不可能用单个数字映射来表示它们。您需要使用两个函数,“左优先级”和“右优先级”,一个用于左侧的运算符,另一个用于右侧的运算符。
从某种意义上讲,第一个问题并不那么严重,因为弗洛伊德(Floyd)算法本身并不一定检测所有语法错误。实际上,运算符优先级解析会擦除不同非终端之间的差异;所有的归约动作都会产生一种通用的非终结符,这可能会丢失区分不同句法情况所需的信息。如果信息丢失导致无法在两个不同的归约之间做出决定,则无法使用运算符优先级来解析语法。但是,信息丢失导致无法检测语法错误的情况更为普遍,这可以被认为是可以接受的。
无论如何,弗洛伊德的算法变得非常流行。它用于构建多种语言的解析器,并产生了一些仍在流行的范例,例如调车场算法。
通常,很难手动计算优先级矩阵,然后将其简化为一对函数。但是在一个特定的领域(代数表达式)中,结果是如此直观,以至于取代了原来的定义。如果要解析不带括号且不带一元运算符的代数表达式,可以通过将运算符分组为优先级来实现。每个级别都分配了两个连续的整数(没有两个级别使用相同的整数),分别用作左优先值和右优先值。
为了区分左关联运算符和右关联运算符,有两个整数:对于左关联级别,左优先级是两个整数中的较大者。对于右关联级别,右优先级更大。
这不是唯一的解决方案。您还会找到许多其他人。例如,很常见的情况是将其描述为单个整数并伴有两个状态的枚举(关联性),如果比较的两个整数相等,则将关联性考虑在内。通常,“简化”不适用于运算符优先级语法,因为不能简单地将括号和一元运算符从语言中轻易地挥舞开来。但是,如果在应用优先级规则的情况下,解析框架已经处理了括号,那可能是可以接受的。
使用运算符优先级解析器处理括号没有问题。当您坚持要求符号的左和右顺序为连续整数(或单个整数和标志)时,就会出现问题:
-
(
总是 移到堆栈上,这意味着它的右优先级高于任何运算符的左优先级。 - 当
(
位于堆栈的顶部时,没有运算符会导致其减少,这意味着其左优先级低于任何运算符的右优先级。 (最终会因匹配的)
而减少)。
一元运算符具有相似的不对称性。前缀运算符也总是移位,因为没有前面要减少的非终结符。但是,如NOT
所示,运算符的优先级不一定比后面的任何运算符高。因此它的左优先级和右优先级可以有很大差异。
(待续)
注意:
-
如果我们只想检查表达式的语法,则可以消除
not_test
和factor
中的递归产生,这可能会引起混淆。我们可以改写:not_test: ( 'not' )* comparison factor: ( '+'|'-'|'~'|atom_expr '**')* atom_expr
但是,这不会产生正确的解析,因为它不会将运算符与其参数关联。例如,从右到左应用求幂是必不可少的。而且,我们通常无法通过某种方式将
not
前缀运算符序列合并为单个运算符来评估not
。 -
这只是说明任意运算符优先级如何。确实没有通用标准。甚至在所有语言中,所有运算符的优先级都相同,因此分组严格从右到左进行。
-
新的Python解析器实际上是解析表达式语法(PEG),也是
pyparsing
实现的形式主义。 PEG与无上下文语法有根本的不同,但是在这个特定的小角落,区别并不重要。 -
令人毛骨悚然的是,ACM认为收取15美元的费用是合理的,这样人们就可以阅读近60年前发表的长达18页的论文。如果您认为有用的话,这里是指向paywall的链接。
是的:
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看到不低于+,但是对我来说,这并不能真正解释为什么它不能解析3 +不能2。
您试图像(3 + not)2这样计算是错误的:“ not”运算符不适合作为“ +”运算符作为操作数。