2016-02-15 9 views
5

我正在編寫一個解析器來解析簡單的算術公式:它只需要(和限制)來支持數字和變量上的+ - * /。例如:將ast.Num轉換爲decimal.Decimal以獲取python中的精度

100.50*num*discount 

它基本上用來計算產品的價格。

這是用python編寫的,我想簡單地使用python自己的解析器。這個想法首先解析輸入AST,然後走在AST,以限制一小部分的AST的節點類型,說:ast.BinOpast.Addast.Numast.Name等等...

目前,它的效果很好,除了ast中的浮點數不精確之外。所以我想將ast的ast.Num節點轉換成一些ast.Call(func=ast.Name(id='Decimal'), ...)。但問題是:ast.Num只包含一個n字段,它是已分析的浮點數。在源代碼中獲取原始數字文字並不容易:How to get source corresponding to a Python AST node?

有什麼建議嗎?

+0

你可以解釋一下,你的意思是*原始數字文字在源代碼*中? – Kasramvd

+0

對不起,應該是* numeric literal *:https://docs.python.org/2/reference/lexical_analysis.html?highlight = literal#numeric-literals – jayven

回答

5

我建議採用兩步法:在第一步中,使用Python的tokenize模塊將源文件中的所有浮點數字文字轉換爲格式爲'Decimal(my_numeric_literal)'的字符串。然後,您可以按照您的建議操作AST。

在標記模塊documentation中還有第一步的配方。爲了避免只鏈路的答案,這裏是從配方中的代碼(與配方本身缺少必要的進口沿):通過檢查的存在

from cStringIO import StringIO 
from tokenize import generate_tokens, untokenize, NAME, NUMBER, OP, STRING 

def is_float_literal(s): 
    """Identify floating-point literals amongst all numeric literals.""" 
    if s.endswith('j'): 
     return False # Exclude imaginary literals. 
    elif '.' in s: 
     return True # It's got a '.' in it and it's not imaginary. 
    elif s.startswith(('0x', '0X')): 
     return False # Must be a hexadecimal integer. 
    else: 
     return 'e' in s # After excluding hex, 'e' must indicate an exponent. 

def decistmt(s): 
    """Substitute Decimals for floats in a string of statements. 

    >>> from decimal import Decimal 
    >>> s = 'print +21.3e-5*-.1234/81.7' 
    >>> decistmt(s) 
    "print +Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7')" 

    >>> exec(s) 
    -3.21716034272e-007 
    >>> exec(decistmt(s)) 
    -3.217160342717258261933904529E-7 

    """ 
    result = [] 
    g = generate_tokens(StringIO(s).readline) # tokenize the string 
    for toknum, tokval, _, _, _ in g: 
     if toknum == NUMBER and is_float_literal(tokval): 
      result.extend([ 
       (NAME, 'Decimal'), 
       (OP, '('), 
       (STRING, repr(tokval)), 
       (OP, ')') 
      ]) 
     else: 
      result.append((toknum, tokval)) 
    return untokenize(result) 

的原始配方確定浮點文字值爲'.'。這不完全是防彈的,因爲它不包括像'1e10'這樣的文字,並且包含像1.0j這樣的虛構文字(您可能想要排除)。我用上面的is_float_literal我自己的版本取代了這個檢查。

嘗試這對你比如字符串,我得到這個:

>>> expr = '100.50*num*discount' 
>>> decistmt(expr) 
"Decimal ('100.50')*num *discount " 

...你現在可以解析到一個AST樹前:

>>> tree = ast.parse(decistmt(expr), mode='eval') 
>>> # walk the tree to validate, make changes, etc. 
... 
>>> ast.dump(tree) 
"Expression(body=BinOp(left=BinOp(left=Call(func=Name(id='Decimal', ... 

最後評價:

>>> from decimal import Decimal 
>>> locals = {'Decimal': Decimal, 'num': 3, 'discount': Decimal('0.1')} 
>>> eval(compile(tree, 'dummy.py', 'eval'), locals) 
Decimal('30.150') 
+0

這似乎是正確的方法,謝謝! – jayven