【问题标题】:Python: Suppress expansion of exponential notation in parse_expr, sympyPython:抑制 parse_expr、sympy 中指数符号的扩展
【发布时间】:2020-06-22 04:37:08
【问题描述】:

输入:

from sympy.parsing.sympy_parser import parse_expr

print(parse_expr('1e10', evaluate=False))

输出:

10000000000.0000

无论指数有多大,它都会扩展科学记数法。但是,我希望是直的1e10

偶数

from sympy import evaluate
with evaluate(False):
    print(parse_expr('1e10', evaluate=False))

返回相同的输出。

我使用parse_expr 将用户友好的表达式(如2 x + 3^2)解析为2*x + 3**2,使用standard_transformations, implicit_multiplication, convert_xor。解析的输出被馈送到python进行评估,同时打印在屏幕上供用户参考。 1e1010000000000.0000 的扩展非常烦人。如何预防?

【问题讨论】:

  • 谢谢,但这不起作用,因为它会将e10 识别为变量但未定义。
  • 我写了一个计算器。当我输入时,例如1e10 m/s^2,它使用 sympy parse_expr 将其解析为 1e10*m/s**2,以便 python 可以评估它。但是,parse_expr('1e10', evaluate=False)(带有一些额外的转换)会将其解析为 10000000000.0000*m/s**2,这是等效的,但是当显示在屏幕上以供用户进行错误检查时,它会变得不必要地长。
  • 这是我遇到这个问题时写的计算器:github.com/chongchonghe/acap。输入1e10 m,python 输入显示10000000000.0*m,但我想要1e10 m1*10^10 m
  • “将1e10 视为数字而不是隐式乘法”似乎可以解释为什么会发生这种情况。可能我对此无能为力。

标签: python sympy scientific-notation


【解决方案1】:

根据讨论,问题不是解析,而是输出是关注的对象。如前所述,无法修改默认的表示方法,但这并不排除将特定令牌转换为应用程序用户可能希望看到的表示的可能性。阅读文档,有一个printing 模块将提供必要的控制级别。

以下是控制Floats 呈现方式的最基本实现,通过扩展提供的StrPrinter

from sympy.printing.str import StrPrinter

class CustomStrPrinter(StrPrinter):
    def _print_Float(self, expr):         
        return '{:.2e}'.format(expr)

现在有了控制输出的能力,给它一个方程

>>> stmt = parse_expr('2*x + 3**2 + 1e10', evaluate=False)
>>> CustomStrPrinter().doprint(stmt)
'2*x + 3**2 + 1.00e+10'

请注意,我提供的实现将输出限制为两位数 - 您需要针对您的特定用例进行修改,因为 this thread 可能会给您一些指示。

另外,咨询parsing transformation 部分(和相关代码)可能会有用。


自然,更深入地研究 sympy 实际处理解析的方式(这实际上是我第一次查看这个库;在我目前尝试回答这个问题之前,我有 的经验),我注意到转换可能有用。

查看parse_expr 是如何实现的,我注意到它调用stringify_expr,并且考虑到转换实际上应用在该位置,它向我表明在转换步骤之前和之后比较tokens 的列表将是一个攻击的好地方。

这样,启动调试器(一些输出被截断)


>>> from sympy.parsing.sympy_parser import parse_expr, standard_transformations
>>> import pdb
>>> pdb.run("parse_expr('1e10', transformations=standard_transformations)")
> <string>(1)<module>()
(Pdb) s
--Call--
> /tmp/env/lib/python3.7/site-packages/sympy/parsing/sympy_parser.py(908)parse_expr()
(Pdb) b 890
(Pdb) c
-> for transform in transformations:
(Pdb) pp tokens
[(2, '1e10'), (0, '')]
(Pdb) clear
(Pdb) b 893
(Pdb) c
> /tmp/env/lib/python3.7/site-packages/sympy/parsing/sympy_parser.py(893)stringify_expr()
-> return untokenize(tokens)
(Pdb) pp tokens
[(1, 'Float'), (53, '('), (2, "'1e10'"), (53, ')'), (0, '')]

(顺便说一句,在 sympy “编译”之前,这些标记将被取消标记回 Python 源代码)

自然地,一个简单的float 会被转换为sympy.core.numbers.Float,并且阅读该文档表明可以控制精度,这向我表明这可能会对输出产生影响,让我们尝试一下:

>>> from sympy.core.numbers import Float
>>> Float('1e10', '1')
1.e+10

到目前为止一切顺利,看起来正是您想要的。然而,默认的auto_number 实现不提供任何东西来切换Float 节点的构造方式,但是考虑到转换只是简单的函数,并且可以在应用auto_number 转换之后根据该输出的特征来实现。可以做到以下几点:

from ast import literal_eval
from tokenize import NUMBER, NAME, OP


def restrict_e_notation_precision(tokens, local_dict, global_dict):
    """
    Restrict input e notation precision to the minimum required.

    Should be used after auto_number transformation, as it depends on
    that transform to add the ``Float`` functional call for this to have
    an effect.
    """

    result = []
    float_call = False

    for toknum, tokval in tokens:
        if toknum == NAME and tokval == 'Float':
            # set the flag
            float_call = True

        if float_call and toknum == NUMBER and ('e' in tokval or 'E' in tokval):
            # recover original number before auto_number transformation
            number = literal_eval(tokval)
            # split the significand from base, while dropping the
            # decimal point before treating length as the precision.
            precision = len(number.lower().split('e')[0].replace('.', ''))
            result.extend([(NUMBER, repr(str(number))), (OP, ','),
                (NUMBER, repr(str(precision)))])
            float_call = False
        else:
            result.append((toknum, tokval))

    return result

现在按照使用 parse_expr 的文档进行自定义转换,如下所示:

>>> from sympy.parsing.sympy_parser import parse_expr, standard_transformations
>>> transforms = standard_transformations + (restrict_e_notation_precision,)
>>> stmt = parse_expr('2.412*x**2 + 1.14e-5 + 1e10', evaluate=False, transformations=transforms)
>>> print(stmt)
2.412*x**2 + 1.14e-5 + 1.0e+10

这看起来正是您想要的。但是,仍有一些情况需要自定义打印机来解决,例如:

>>> stmt = parse_expr('3.21e2*x + 1.3e-3', evaluate=False, transformations=transforms)
>>> stmt
321.0*x + 0.0013

这又是由于 Python 中 float 类型的默认 repr 施加的限制。使用此版本的 CustomStrPrinter 来检查并利用我们在此处自定义转换中指定的精度将解决此问题:

from sympy.printing.str import StrPrinter

class FloatPrecStrPrinter(StrPrinter):
    """
    A printer that checks for custom precision and output concisely.
    """

    def _print_Float(self, expr):
        if expr._prec != 53:  # not using default precision
            return ('{:.%de}' % ((int(expr._prec) - 5) // 3)).format(expr)
        return super()._print_Float(expr) 

演示:

>>> from sympy.parsing.sympy_parser import parse_expr, standard_transformations
>>> transforms = standard_transformations + (restrict_e_notation_precision,)
>>> stmt = parse_expr('3.21e2*x + 1.3e-3 + 2.7', evaluate=False, transformations=transforms)
>>> FloatPrecStrPrinter().doprint(stmt)
'3.21e+2*x + 1.3e-3 + 2.7'

需要注意的是,我上面的解决方案利用了浮点数的各种特性——如果在计算中使用输入表达式,它可能不会给出任何正确的输出,并且使用的数字和任何方程主要用作粗略的指导而且我没有对所有事情都进行任何边缘情况调查 - 我将把它留给你,因为这已经花费了我很多时间来深入研究并写下整个事情 - 例如。您可能希望将其用于向用户显示,但在内部保留其他纯粹的表达式(即没有此额外转换)以进行算术运算。

无论如何,往前走,记下所依赖的库的实现细节绝对是有用的,不要害怕使用调试器来找出真正发生的事情并充分利用图书馆制造商提供给用户的钩子供他们扩展。

【讨论】:

  • 美化这个答案,它带来了其他的不便。例如,2.3 * x 将变为 2.30e+0*x,因为所有浮点数都以科学计数法显示。我提出这个问题的最终目标是找到一种方法让parse_expr 保持1e10 原样,因为纯python 理解这一点,同时进行所有其他解析。
  • @JordanHe 修改了答案以完全解决您的问题。另外关于您的评论:我确实清楚地注意到“您需要针对您的特定用例进行修改”,尽管我的附加答案现在也有一个修改后的形式。您需要在您和其他人已发表的研究成果的基础上进行进一步的工作,以构建您想要的解决方案,因为我只能假设这么多。
  • 非常感谢!您的解决方案功能齐全。我肯定会将您修改后的答案的第一部分实施到我的代码中,用于实际计算和用户显示。首要任务是确保计算结果与用户显示相匹配。至于第二部分,由于它只是打印浮动的格式,所以只应用于用户显示是安全的。此外,'3.21e2' 的问题并不是什么大问题,因为当您将指数增加到 3(>= 精度)时,e 符号将保留。无论如何,用户都不会输入高精度数字。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-07-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-04-02
  • 1970-01-01
相关资源
最近更新 更多