将 ast.Num 转换为 decimal.Decimal 以获得 python 中的精度
Convert ast.Num to decimal.Decimal for precision in python
我目前正在编写一个解析器来解析简单的算术公式:它只需要(并限制)支持 +-*/ 数字和变量。例如:
100.50*num*discount
主要用于计算产品价格。
这是用 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
字段,该字段是已经解析的浮点数。而且在源代码中获取原始数字字面量并不容易:How to get source corresponding to a Python AST node?
有什么建议吗?
我建议采用两步法:第一步,使用 Python 的 tokenize
模块将源代码中的所有浮点数字文字转换为以下形式的字符串'Decimal(my_numeric_literal)'
。然后您可以按照您建议的方式处理 AST。
在标记化模块 documentation 中甚至还有第一步的秘诀。为避免仅 link 的答案,这里是该配方的代码(以及配方本身缺少的必要导入):
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')
我目前正在编写一个解析器来解析简单的算术公式:它只需要(并限制)支持 +-*/ 数字和变量。例如:
100.50*num*discount
主要用于计算产品价格。
这是用 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
字段,该字段是已经解析的浮点数。而且在源代码中获取原始数字字面量并不容易:How to get source corresponding to a Python AST node?
有什么建议吗?
我建议采用两步法:第一步,使用 Python 的 tokenize
模块将源代码中的所有浮点数字文字转换为以下形式的字符串'Decimal(my_numeric_literal)'
。然后您可以按照您建议的方式处理 AST。
在标记化模块 documentation 中甚至还有第一步的秘诀。为避免仅 link 的答案,这里是该配方的代码(以及配方本身缺少的必要导入):
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')