`
把酒泯恩仇
  • 浏览: 26429 次
  • 性别: Icon_minigender_1
  • 来自: 广州
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

10行代码搞定表达式解析【花了一个小时翻译的】

阅读更多

查看原文有更好的格式:


http://www.ibaiyang.org/2012/08/03/parsing-expression-by-precedence-climbing/

 

摘自 Eli Bendersky’s website

就此问题,我之前讨论过使用递归下降解析器(recursive descent parsers),特别是当一门语言有多级运算优先级时。

有很多方法可以解决这个问题。在维基百科上的一篇文章提到了三种算法:Shunting Yard, top-down operator precedence (TDOP) and 优先爬山(precedence climbing)。今天的主题就是讲解一下第三种算法,因为它在实践中经常被用到。

优先爬山–目的  

在了解优先爬山算法前,没有必要对其他表达式解析器算法熟悉。实际上,它是众多算法中最简单的。在详细讲述前,我先简单的说一下此算法欲实现的目的,之后,我再详细讲述,最后,贴出用python实现的代码。

此算法的基本思想是:一个表达式通常是由若干具有低优先级的子表达式构成。

一个简单的例子:

2 + 3 * 4 * 5 - 6

假设(+和-)和(*和/)分别的优先级是1,2【数字越大,优先级越大,故先运算】,那么我们有:

2 + 3 * 4 * 5 - 6

|---------------|   : prec 1
    |-------|       : prec 2

三个数连乘的子表达式有最小的优先级2.整个表达式具有最小优先级1.
给一个稍微复杂的例子,此列中,包含指数运算,令其优先级为3:

2 + 3 ^ 2 * 3 + 4

|---------------|   : prec 1
    |-------|       : prec 2
    |---|           : prec 3

结合律(Associativity)

二元运算符除了有优先级,同时也有结合律。譬如,同一级运算符中,具有左结合律的二元运算符从左向右运算;具有右结合律的二元操作符从右向左运算。
下面给出一些例子吧,例如加好(+)是左结合律,因此

2 + 3 + 4

(2 + 3) + 4

等价;另一方面,如指数运算(^)具有右结合律,因此

2 ^ 3 ^ 4

2 ^ (3 ^ 4)

等价。
当然,优先爬山算法能正确的处理结合律。

带有括号的子表达式
最后,我们知道,括号能够任意的组合子表达式,打破运算符号的优先级,例如:

2 * (3 + 5) * 7

爬山算法—如何奏效

首先,我们定义一些术语。元(atoms)可以是数字或则带有括号的子表达式。表达式(expression)是由元(atom)同二元运算符构成。

此算法是运算符驱动的它基本的步骤就是读取下一个元和查看紧跟其后的运算符。如果后面的运算符优先级小于当前步骤的优先级,此算法返回;否则,他在一个循环中反复的调用自己去处理子表达式。

下面是伪代码:

compute_expr(min_prec):
  result = compute_atom()

  while cur token is a binary operator with precedence >= min_prec:
    prec, assoc = precedence and associativity of current token
    if assoc is left:
      next_min_prec = prec + 1
    else:
      next_min_prec = prec
    rhs = compute_expr(next_min_prec)
    result = compute operator(result, rhs)

  return result

每一次递归调用处理具有同一优先级运算符号构成的元的子表达式。

一个例子

让我们通过一个例子,对此算法有一个直观的认识吧。

2 + 3 ^ 2 * 3 + 4

算法从调用compute_expr(1)开始,因为1具有最小的优先级。下面是此例子的调用过程:

* compute_expr(1)                # Initial call on the whole expression
  * compute_atom() --> 2
  * compute_expr(2)              # Loop entered, operator '+'
    * compute_atom() --> 3
    * compute_expr(3)
      * compute_atom() --> 2
      * result --> 2             # Loop not entered for '*' (prec < '^')     * result = 3 ^ 2 --> 9
    * compute_expr(3)
      * compute_atom() --> 3
      * result --> 3             # Loop not entered for '+' (prec < '*')     * result = 9 * 3 --> 27
  * result = 2 + 27 --> 29
  * compute_expr(2)              # Loop entered, operator '+'
    * compute_atom() --> 4
    * result --> 4               # Loop not entered - end of expression
  * result = 29 + 4 --> 33

处理优先级

注意到此算法针对每一个二元操作符进行一次递归调用。其中一些生命期非常的短—读取一个元(atom),然后直接返回,因为没有进入while循环(发生在第二步);另一些生命期比较长。初始调用compute_expr 将计算整个表达式。

while循环是一个必要的组成成分。它保证了当前compute_expr 会处理连续的同一优先级的操作符。

处理结合律

我认为,这个算法最吸引人的地方是它非常的简单,并且能够很好的处理结合律。不管什么情况下,此算法总是设置下一次调用的优先级是当前优先级,或则在此基础上加1。

我们举一个例子来看其如何处理的吧

8 * 9 * 10

  ^
  |

箭头所指向的地方是compute_expr 运行到的位置,并且已经进入了while循环,此时prec=2.因为*是左结合律,故next_min_prec=2 + 1。在读入一个数据后,调用
compute_expr(3),看下一个*

8 * 9 * 10

      ^
      |

因为*的优先级是2,然而min_prec等于3,所以没有进入while循环,直接返回。犹如

(8 * 9) * 10

相反,对于以下实例:

8 ^ 9 ^ 10

^的优先级是3,并且其是右结合律,所以min_prec是3,不会加一. 这就意味着在返回调用者之前,还会去读取下一个^运算符,犹如

8 ^ (9 ^ 10)

处理子表达式

伪代码没有解释如何处理带有括号的子表达式。考虑如下表达式:

2000 * (4 - 3) / 100

while循环没有很清楚的告诉如何处理这个问题。当然解决问题的关键是compute_atom。当它读入一个左括号时,它就知道有一个子表达式紧随其后,

所以它调用compute_expr 去处理这个子表达式(直到遇见右括号才返回),然后才将结果作为一个元(atom)返回给compute_atom。所以compute_expr
根本就没有感觉到括号的存在。

python实现

为了简单,它十分的短,你可以很轻松的将其扩展到你实际中的语言中。接下来我讲分几块来呈现这些实现方式。

开始我们需要拥有一个简单的tokenizer类去将文本分为tokens和保持一些状态。语法非常简单:数字,基本的运算符+,-,*,/,^和括号。

  1. Tok = namedtuple('Tok''name value')  
  2.   
  3. class Tokenizer(object):  
  4.     """ Simple tokenizer object. The cur_token attribute holds the current 
  5.         token (Tok). Call get_next_token() to advance to the 
  6.         next token. cur_token is None before the first token is 
  7.         taken and after the source ends. 
  8.     """  
  9.     TOKPATTERN = re.compile("\s*(?:(\d+)|(.))")  
  10.   
  11.     def __init__(self, source):  
  12.         self._tokgen = self._gen_tokens(source)  
  13.         self.cur_token = None  
  14.   
  15.     def get_next_token(self):  
  16.         """ Advance to the next token, and return it. 
  17.         """  
  18.         try:  
  19.             self.cur_token = self._tokgen.next()  
  20.         except StopIteration:  
  21.             self.cur_token = None  
  22.         return self.cur_token  
  23.   
  24.     def _gen_tokens(self, source):  
  25.         for number, operator in self.TOKPATTERN.findall(source):  
  26.             if number:  
  27.                 yield Tok('NUMBER', number)  
  28.             elif operator == '(':  
  29.                 yield Tok('LEFTPAREN''(')  
  30.             elif operator == ')':  
  31.                 yield Tok('RIGHTPAREN'')')  
  32.             else:  
  33.                 yield Tok('BINOP', operator)  

接下来,是compute_atom实现方式

  1. def compute_atom(tokenizer):  
  2.     tok = tokenizer.cur_token  
  3.     if tok.name == 'LEFTPAREN':  
  4.         tokenizer.get_next_token()  
  5.         val = compute_expr(tokenizer, 1)  
  6.         if tokenizer.cur_token.name != 'RIGHTPAREN':  
  7.             parse_error('unmatched "("')  
  8.         tokenizer.get_next_token()  
  9.         return val  
  10.     elif tok is None:  
  11.             parse_error('source ended unexpectedly')  
  12.     elif tok.name == 'BINOP':  
  13.         parse_error('expected an atom, not an operator "%s"' % tok.value)  
  14.     else:  
  15.         assert tok.name == 'NUMBER'  
  16.         tokenizer.get_next_token()  
  17.         return int(tok.value)  

它处理元信息(在我们的例子中是整数),包括带有括号的子表达式。

然后是,compute_expr 的实现方式,和上面的伪代码十分的相识

  1. # For each operator, a (precedence, associativity) pair.  
  2. OpInfo = namedtuple('OpInfo''prec assoc')  
  3.   
  4. OPINFO_MAP = {  
  5.     '+':    OpInfo(1'LEFT'),  
  6.     '-':    OpInfo(1'LEFT'),  
  7.     '*':    OpInfo(2'LEFT'),  
  8.     '/':    OpInfo(2'LEFT'),  
  9.     '^':    OpInfo(3'RIGHT'),  
  10. }  
  11.   
  12. def compute_expr(tokenizer, min_prec):  
  13.     atom_lhs = compute_atom(tokenizer)  
  14.   
  15.     while True:  
  16.         cur = tokenizer.cur_token  
  17.         if (cur is None or cur.name != 'BINOP'  
  18.                         or OPINFO_MAP[cur.value].prec < min_prec):  
  19.             break  
  20.   
  21.         # Inside this loop the current token is a binary operator  
  22.         assert cur.name == 'BINOP'  
  23.   
  24.         # Get the operator's precedence and associativity, and compute a  
  25.         # minimal precedence for the recursive call  
  26.         op = cur.value  
  27.         prec, assoc = OPINFO_MAP[op]  
  28.         next_min_prec = prec + 1 if assoc == 'LEFT' else prec  
  29.   
  30.         # Consume the current token and prepare the next one for the  
  31.         # recursive call  
  32.         tokenizer.get_next_token()  
  33.         atom_rhs = compute_expr(tokenizer, next_min_prec)  
  34.   
  35.         # Update lhs with the new value  
  36.         atom_lhs = compute_op(op, atom_lhs, atom_rhs)  
  37.   
  38.     return atom_lhs  

唯一不同的一点是这个代码非常明显的处理token信息。基本上是遵循“递归向下调用”原则。每一次递归调用只能获取当前的token,存放在tokenizer.cur_tok里,并且确保读取所有的tokens且被处理(通过不断的调用tokenizer.get_next_token()).

还有额外的一小块没有提到。compute_op 是简单的执行了数字运算:

  1. def compute_op(op, lhs, rhs):  
  2.     lhs = int(lhs); rhs = int(rhs)  
  3.     if op == '+':   return lhs + rhs  
  4.     elif op == '-'return lhs - rhs  
  5.     elif op == '*'return lhs * rhs  
  6.     elif op == '/'return lhs / rhs  
  7.     elif op == '^'return lhs ** rhs  
  8.     else:  
  9.         parse_error('unknown operator "%s"' % op)  

其他的资源

这里还有一些我发现的额外的资料

 

 


-----------------打造高质量的文章 更多关注 把酒泯恩仇---------------

为了打造高质量的文章,请  推荐  一下吧。。。。谢谢了,请关注我后续的文章,会更精彩哦

请关注sina微博:http://weibo.com/baiyang26

把酒泯恩仇官方博客:http://www.ibaiyang.org 【推荐用google reader订阅】

把酒泯恩仇官方豆瓣:http://www.douban.com/people/baiyang26/

如果您想转载本博客,请注明出处

如果您对本文有意见或者建议,欢迎留言

8
2
分享到:
评论
3 楼 把酒泯恩仇 2012-12-13  
boyfei1 写道

神马情况。。。。。。。
2 楼 boyfei1 2012-12-13  
1 楼 boyfei1 2012-12-13  
[color=red][/color][/i][/u][u]
引用
引用
[i]
[u][/u]
引用
[img][/img][url][/url][flash=200,200][/flash]
[b][/b]

相关推荐

Global site tag (gtag.js) - Google Analytics