评估期间的循环算法

Round arithmetic during evaluation

问题

评估算术时有多个步骤 (PEMDAS)。我知道您可以评估一个操作然后对其进行舍入,但有时您需要对数据进行舍入以在整个评估过程中永远不会超过某个精度。这让我想到了我的问题:你怎么能在评估期间的每一步都四舍五入,而不是仅仅在最后?

例子

对于第一个示例,我们将使用简单运算 0.125/0.375 并四舍五入到两位小数。

# This operation evaluates to 1/3
>>> 0.125/0.375
0.3333333333333333

# If we wanted to round it we could just do
>>> round(0.125/0.375, 2)
0.33

# But if we wanted to round at every step of PEMDAS the following would be necessary
>>> round(round(0.125, 2)/round(0.375, 2), 2)
0.32

# Same equation as above but written as (1/8)/(3/8)
>>> round(round(round(1, 2)/round(8, 2), 2)/round(round(3, 2)/round(8, 2), 2), 2)
0.32

如您所见,如果在每一步而不是最后都执行舍入,您会得到不同的结果。

尽管这种方法有点麻烦,但确实可以完成工作。但是当方程不是硬编码而是从用户那里接收时就会出现问题:

# Rounding cannot be applied here in the same way that we did above
>>> eval(input("Arithmetic: "))
Arithmetic: (1/8)/(3/8)
0.3333333333333333

用例

乍一看这似乎没什么用,但实际上对很多事情都非常有价值。 这是一个简单的例子,其中每一步都需要四舍五入才能找到函数的漏洞:

# undefined.py

from math import *
import numpy as np


function = input("Function in terms of x: ")


def is_undefined(x):
  x = round(x, 2)   # To deal with minor Python inaccuracies (ex: 1.000000000000001)
  try:
    eval(function)
    return False
  except ZeroDivisionError:
    return True


undefined = [x for x in np.linspace(-5, 5, 1001) if is_undefined(float(x))]
print(undefined)
# Works perfectly!
>>> python undefined.py
Function in terms of x: (x**2)*(x-2)/(x-2)
[2.0]

# Unable to find the hole at x=pi
>>> python undefined.py
Function in terms of x: (x**2)*(2*x - 2*pi)/(x - pi)
[]

decimal模块提供了一个Decimal类型,可以对其进行配置,使所有算术运算都四舍五入到一定的小数位数:

>>> import decimal as d
>>> d.setcontext(d.Context(prec=2))
>>> x = d.Decimal(0.125)
>>> y = d.Decimal(0.375)
>>> x / y
Decimal('0.33')

您可以使用一元运算 + 强制舍入除法之前的数字,该运算通常什么都不做,但在这种情况下,它会应用当前上下文中的精度,将结果更改为(当然更不准确):

>>> (+x) / (+y)
Decimal('0.32')

因此,对于来自用户输入的表达式的解决方案可能是将变量 x 的所有数字文字和实例替换为具有相同值的 Decimal 对象:这里我使用了常规表达式来做到这一点,并使用一元 + 在操作前强制舍入。

import decimal as d
import re

d.setcontext(d.Context(prec=2))

function = input("Function in terms of x: ")
function = re.sub(r'([0-9]+(\.[0-9]+)?|x)', r'(+d.Decimal())', function)

# ...

请注意,不再需要编写 x = round(x, 2),因为表达式本身会强制 x 进行舍入。

您可能正在寻找符号数学,例如 Sympy,它可能可以满足您的真正需求-

具体而言,不要混淆超越数(如 pi 和 e)或等待将不可约分数化为十进制 space,直到被要求 evaluate to a decimal

>>> from sympy import *
>>> expr = "(1/8)/(3/8)"
>>> simplify(expr)           # precise value
1/3
>>> simplify(expr).evalf()   # decimal aliasing
0.333333333333333
>>> N("(1/8)/(3/8)")         # using sympy.N()
0.333333333333333

这也可以用来解方程

>>> x = symbols("x", real=True)
>>> solve(x**2 - 1)                         # simple solution
[-1, 1]
>>> solve(x**2 - pi)                        # more complex solution
[-sqrt(pi), sqrt(pi)]
>>> [N(expr) for expr in solve(x**2 - pi)]  # decimal approximation
[-1.77245385090552, 1.77245385090552]

这也可以用于(可能是邪恶的)Python 结构

>>> [N(x * pi) for x in range(10)]          # lots of approximations!
[0, 3.14159265358979, 6.28318530717959, 9.42477796076938, 12.5663706143592, 15.7079632679490, 18.8495559215388, 21.9911485751286, 25.1327412287183, 28.2743338823081]