将ast.Num转换为decimal.Decimal以获得python中的精度

jay*_*ven 5 python floating-point decimal abstract-syntax-tree

我正在编写一个解析器来解析简单的算术公式:它只需要(和限制)来支持数字和变量上的+ - */.例如:

100.50*num*discount
Run Code Online (Sandbox Code Playgroud)

它基本上用于计算产品价格.

这是用python编写的,我想简单地使用python自己的解析器.这个想法首先解析输入AST,然后走在AST,以限制一小部分的AST的节点类型,比如:ast.BinOp,ast.Add,ast.Num,ast.Name等...

目前它运作良好,除了ast中的浮点数不精确.所以我想将ast的ast.Num节点转换成一些节点ast.Call(func=ast.Name(id='Decimal'), ...).但问题是:ast.Num只包含一个n已解析的浮点数字段.在源代码中获取原始数字文字并不容易:如何获取对应于Python AST节点的源代码

有什么建议吗?

Mar*_*son 5

我建议采用两步法:在第一步中,使用Python的tokenize模块将源中的所有浮点数字文字转换为表单的字符串'Decimal(my_numeric_literal)'.然后你可以按照你建议的方式处理AST.

令牌化模块文档中的第一步甚至还有一个配方.为了避免仅链接答案,这里是该配方的代码(以及配方本身缺少的必要导入):

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)
Run Code Online (Sandbox Code Playgroud)

原始配方通过检查值中是否存在a '.'来标识浮点文字.这并不完全是防弹,因为它排除了像这样的文字'1e10',并且包括像1.0j你想要排除的想象文字.我用is_float_literal上面的自己的版本替换了那张支票.

在你的示例字符串上尝试这个,我得到这个:

>>> expr = '100.50*num*discount'
>>> decistmt(expr)
"Decimal ('100.50')*num *discount "
Run Code Online (Sandbox Code Playgroud)

...您现在可以像以前一样解析为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', ...
Run Code Online (Sandbox Code Playgroud)

并最终评估:

>>> from decimal import Decimal
>>> locals = {'Decimal': Decimal, 'num': 3, 'discount': Decimal('0.1')}
>>> eval(compile(tree, 'dummy.py', 'eval'), locals)
Decimal('30.150')
Run Code Online (Sandbox Code Playgroud)