Python:抑制 parse_expr 中指数符号的扩展,sympy

Python: Suppress expansion of exponential notation in parse_expr, sympy

输入:

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))

returns同样的输出。

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

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

以下是控制 Float 呈现方式的最基本实现,通过扩展提供的 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 is implemented, I noted that it calls 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 节点的构造方式,但是考虑到转换只是函数,并且可以根据 [=27 之后的输出特征来实现=] 应用了变换。可以执行以下操作:

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'

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

无论如何,今后,注意所依赖的库的实现细节绝对是有用的,不要害怕使用调试器来弄清楚引擎盖下到底发生了什么,并使完整的使用图书馆制造商提供给用户的挂钩,供他们扩展。