如何在Python中找到精度较低的浮点值的原始文本表示?

如何在Python中找到精度较低的浮点值的原始文本表示?,python,python-3.x,floating-point,rounding,Python,Python 3.x,Floating Point,Rounding,我遇到了一个问题,在Python中显示从外部数据源加载的float值(它们是32位浮点,但这也适用于精度较低的浮点) (如果重要的话——这些值是由人类在C/C++中输入的,因此与任意计算值不同,舍入数的偏差可能不是故意的,但不能忽略,因为这些值可能是常量,如M_PI,或乘以常量) 由于CPython使用更高的精度(通常为64位),作为较低精度浮点输入的值可能会显示32位浮点的精度损失,其中64位浮点将显示舍入值 例如: 在大多数情况下,简单地将值四舍五入到一些任意精度是可行的,但可能是不正确的,

我遇到了一个问题,在Python中显示从外部数据源加载的
float

(它们是32位浮点,但这也适用于精度较低的浮点)

(如果重要的话——这些值是由人类在C/C++中输入的,因此与任意计算值不同,舍入数的偏差可能不是故意的,但不能忽略,因为这些值可能是常量,如
M_PI
,或乘以常量)

由于CPython使用更高的精度(通常为64位),作为较低精度浮点输入的值可能会显示32位浮点的精度损失,其中64位浮点将显示舍入值

例如:

在大多数情况下,简单地将值四舍五入到一些任意精度是可行的,但可能是不正确的,因为它可能会丢失有效值,例如:
0.00000001

这方面的一个示例可以通过打印转换为32位浮点的浮点来显示

def as_float_32(f):
    from struct import pack, unpack
    return unpack("f", pack("f", f))[0]

print(0.025)               #  --> 0.025
print(as_float_32(0.025))  #  --> 0.02500000037252903

所以我的问题是:

在不做假设或失去精度的情况下,获取32位浮点原始表示的最有效、最直接的方法是什么?

换言之,如果我有一个包含32位浮点的数据源,这些数据源最初是由一个人作为舍入值输入的(上面的示例),但是将它们表示为更高精度的值会暴露出作为32位浮点的值是原始值的近似值

我想反转这个过程,从32位浮点数据中得到整数,但不失去32位浮点给我们的精度。(这就是为什么简单的四舍五入不是一个好的选择)


您可能希望这样做的原因示例:

  • 生成API文档,Python从内部使用单精度浮点的C-API中提取值
  • 当人们需要读取/查看生成的数据值时,这些数据恰好以单精度浮点形式提供
在这两种情况下,重要的是不要失去显著的精度,或显示人类无法一目了然读取的值


  • 更新,我已经提出了一个解决方案,我将包括作为一个答案(供参考和展示其可能性),但高度怀疑它是一个有效或优雅的解决方案

  • 当然,您不知道所使用的符号:
    0.1f
    0.1f
    1e-1f
    ,在输入的位置,这不是本问题的目的


至少在python3中,您可以使用
。as\u integer\u ratio
。这并不完全是一个字符串,但浮点定义本身并不适合在“有限”字符串中给出精确的表示

因此,通过保存这两个数字,您将永远不会失去精度,因为这两个数字正好代表保存的浮点数。(只需将第一个除以第二个即可得到值)


例如,使用numpy数据类型(非常类似于c数据类型):

所有这些计算的结果是:

[ 0.09997559] # Float16 with integer-ratio
[ 0.09997559] # Float16 reference
[ 0.1] # Float32
[ 0.1] # Float64

这里有一个我想出的解决方案,它可以工作(就我所知是完美的),但效率不高

它通过增加小数位数进行舍入,并在舍入和非舍入输入匹配时返回字符串(当与转换为较低精度的值进行比较时)

代码:

产出:

0.02500000037252903 -> 0.025
0.03999999910593033 -> 0.04
0.05000000074505806 -> 0.05
0.30000001192092896 -> 0.3
0.9800000190734863 -> 0.98
1.2000000476837158 -> 1.2
4096.2998046875 -> 4096.3

可能您正在寻找的是:

“十进制”是基于一个浮点数模型,该模型是为人们着想而设计的,并且必然有一个至高无上的指导原则——计算机必须提供一种与人们在学校学习的算法相同的算法

我认为(四舍五入到给定的小数位数)和(去掉尾随的0)是您需要的

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from decimal import Decimal

data = (
    0.02500000037252903,
    0.03999999910593033,
    0.05000000074505806,
    0.30000001192092896,
    0.9800000190734863,
    )

for f in data:
    dec = Decimal(f).quantize(Decimal('1.0000000')).normalize()
    print("Original %s -> %s" % (f, dec))
结果:

Original 0.0250000003725 -> 0.025
Original 0.0399999991059 -> 0.04
Original 0.0500000007451 -> 0.05
Original 0.300000011921 -> 0.3
Original 0.980000019073 -> 0.98

您希望解决与Python的
repr
解决的问题基本相同的问题,即查找舍入到给定浮点的最短十进制字符串。除了在您的例子中,浮点不是IEEE 754二进制64(“双精度”)浮点,而是IEEE 754二进制32(“单精度”)浮点

为了记录在案,我当然应该指出,检索原始字符串表示是不可能的,因为例如字符串
'0.10'
'0.1'
'1e-1'
'10e-2'
都被转换为相同的浮点(或者在本例中是
float32
)。但在适当的条件下,我们仍然可以希望生成一个与原始字符串具有相同十进制值的字符串,这就是我下面要做的

你在回答中概述的方法或多或少是有效的,但它可以简化一点

首先,一些界限:当涉及到单精度浮点的十进制表示时,有两个神奇的数字:
6
9
6
的重要意义在于,任何具有6个或更少有效十进制数字的十进制数字字符串(不太大,也不太小)将通过单精度IEEE 754浮点正确往返:即,将该字符串转换为最接近的
float32
,然后将该值转换回最接近的
6
-位十进制字符串,将生成一个与原始值相同的字符串。例如:

>>> x = "634278e13"
>>> y = float(np.float32(x))
>>> y
6.342780214942106e+18
>>> "{:.6g}".format(y)
'6.34278e+18'
(这里所说的“不要太大,不要太小”,我的意思是应该避免
float32
的下溢和上溢范围。上述属性适用于所有正常值。)

这意味着对于您的问题,如果原始字符串有6个或更少的数字,我们可以通过简单地将值格式化为6个有效数字来恢复它。因此,如果您只关心恢复具有6个或更少有效小数位数的字符串,那么您可以停止阅读这里的内容:一个简单的
'{.6g}'。格式(x)
就足够了。如果您想更全面地解决问题,请继续阅读

圆形的
0.02500000037252903 -> 0.025
0.03999999910593033 -> 0.04
0.05000000074505806 -> 0.05
0.30000001192092896 -> 0.3
0.9800000190734863 -> 0.98
1.2000000476837158 -> 1.2
4096.2998046875 -> 4096.3
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from decimal import Decimal

data = (
    0.02500000037252903,
    0.03999999910593033,
    0.05000000074505806,
    0.30000001192092896,
    0.9800000190734863,
    )

for f in data:
    dec = Decimal(f).quantize(Decimal('1.0000000')).normalize()
    print("Original %s -> %s" % (f, dec))
Original 0.0250000003725 -> 0.025
Original 0.0399999991059 -> 0.04
Original 0.0500000007451 -> 0.05
Original 0.300000011921 -> 0.3
Original 0.980000019073 -> 0.98
>>> x = "634278e13"
>>> y = float(np.float32(x))
>>> y
6.342780214942106e+18
>>> "{:.6g}".format(y)
'6.34278e+18'
>>> x = np.float32(3.14159265358979)
>>> x
3.1415927
>>> np.float32('{:.9g}'.format(x)) == x
True
def original_string(x):
    for places in range(6, 10):  # try 6, 7, 8, 9
        s = '{:.{}g}'.format(x, places)
        y = np.float32(s)
        if x == y:
            return s
    # If x was genuinely a float32, we should never get here.
    raise RuntimeError("We should never get here")
>>> original_string(0.02500000037252903)
'0.025'
>>> original_string(0.03999999910593033)
'0.04'
>>> original_string(0.05000000074505806)
'0.05'
>>> original_string(0.30000001192092896)
'0.3'
>>> original_string(0.9800000190734863)
'0.98'
>>> x = 2.0**87
>>> x
1.5474250491067253e+26
>>> s = '{:.8g}'.format(x)
>>> s
'1.547425e+26'
>>> np.float32(s) == x
False
>>> np.float32('1.5474251e+26') == x
True
>>> x = 2**-96.
>>> x
1.262177448353619e-29
>>> s = '{:.8g}'.format(x)
>>> s
'1.2621774e-29'
>>> np.float32(s) == x
False
>>> np.float32('1.2621775e-29') == x
True
def original_string(x):
    """
    Given a single-precision positive normal value x,
    return the shortest decimal numeric string which produces x.
    """
    # Deal with the three awkward cases.
    if x == 2**-96.:
        return '1.2621775e-29'
    elif x == 2**87:
        return '1.5474251e+26'
    elif x == 2**90:
        return '1.2379401e+27'

    for places in range(6, 10):  # try 6, 7, 8, 9
        s = '{:.{}g}'.format(x, places)
        y = np.float32(s)
        if x == y:
            return s
    # If x was genuinely a float32, we should never get here.
    raise RuntimeError("We should never get here")
>>> repr(numpy.float32(0.0005000000237487257))
'0.0005'
>>> repr(numpy.float32(0.02500000037252903))
'0.025'
>>> repr(numpy.float32(0.03999999910593033))
'0.04'