我遇到了一个问题,在Python中显示浮点值,从外部数据源加载(它们是32位浮点数,但这也适用于较低精度的浮点数).
(如果它很重要 – 这些值是由C/C++中的人类输入的,因此与任意计算值不同,可能不会忽略与圆数的偏差,但不能忽略,因为值可能是常数,如M_PI或乘以通过常数).
由于CPython使用更高的精度(通常为64位),因此作为较低精度浮点输入的值可能会重现()显示精度损失为32位浮点数,其中64位浮点数将显示舍入值.
例如:
# Examples of 32bit float's displayed as 64bit floats in CPython.
0.0005 -> 0.0005000000237487257
0.025 -> 0.02500000037252903
0.04 -> 0.03999999910593033
0.05 -> 0.05000000074505806
0.3 -> 0.30000001192092896
0.98 -> 0.9800000190734863
1.2 -> 1.2000000476837158
4096.3 -> 4096.2998046875
在大多数情况下,简单地将值舍入为某些任意精度都可以工作,但可能不正确,因为它可能会丢失重要值,例如: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,这不是这个问题的目的.
仅仅为了记录,我当然应该指出,检索原始字符串表示是不可能的,因为例如字符串’0.10′,’0.1′,’1e-1’和’10e-2’都被转换为相同的float(或者在本例中为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}’.format(x)就足够了.如果您想更一般地解决问题,请继续阅读.
对于另一个方向的往返,我们有相反的属性:给定任何单精度浮点数x,将该浮点数转换为9位十进制字符串(舍入到最接近,一如既往),然后将该字符串转换回单个 – 精确浮点数,将始终精确恢复该浮点数的值.
>>> x = np.float32(3.14159265358979)
>>> x
3.1415927
>>> np.float32('{:.9g}'.format(x)) == x
True
与您的问题的相关性是,总是至少有一个9位数的字符串向x舍入,因此我们永远不必超过9位.
现在我们可以按照您在答案中使用的相同方法:首先尝试6位数字符串,然后是7位数字,然后是8位数字.如果这些都不起作用,那么9位数的字符串肯定会由上面的字符串组成.这是一些代码.
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'
但是,上面提到了一些警告.
>首先,对于我们使用的关键属性,我们必须假设np.float32始终执行正确的舍入.这可能是也可能不是,取决于操作系统. (即使在相关操作系统调用声称被正确舍入的情况下,仍可能存在声称无效的极端情况.)在实践中,np.float32可能足够接近正确舍入而不会导致问题,但为了完全放心,你想要知道它是正确的圆形.
>其次,上述不适用于低于正常范围的值(因此对于float32,任何小于2 ** – 126的值).在低于正常范围内,6位十进制数字字符串将通过单精度浮点数正确往返是不正确的.如果你关心次正规,你需要在那里做一些更复杂的事情.
>第三,上面有一个非常微妙(和有趣!)的错误几乎无关紧要.我们使用的字符串格式总是将x舍入到最接近的位数 – 数字十进制字符串到x的真值.但是,我们想知道是否有任何位数 – 数字十进制字符串回转到x.我们隐含地假设(看似显而易见的)事实,即如果有任何位数 – 数字十进制字符串舍入到x,则最接近的位数 – 数字十进制字符串舍入到x.这几乎是正确的:从属性得出的是,围绕x的所有实数的区间在x周围是对称的.但是这种对称性在一种特定情况下失败,即当x是2的幂时.
因此,当x是2的精确幂时,(例如)最接近x的最接近的8位十进制字符串可能(但不太可能)不会舍入到x,但是仍然有一个8位十进制字符串舍入到x.你可以对float32范围内发生这种情况的情况进行详尽的搜索,结果发现正好有三个x值,即x = 2 ** – 96,x = 2 ** 87并且x = 2 ** 90.对于7位数字,没有这样的值. (对于6位和9位数字,这种情况永远不会发生.)让我们仔细看看x = 2 ** 87的情况:
>>> x = 2.0**87
>>> x
1.5474250491067253e+26
我们将最接近的8位十进制值取为x:
>>> s = '{:.8g}'.format(x)
>>> s
'1.547425e+26'
事实证明,这个值不会回到x:
>>> np.float32(s) == x
False
但是它的下一个8位十进制数字符号确实如下:
>>> np.float32('1.5474251e+26') == x
True
同样,这是x = 2 ** – 96的情况:
>>> 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
因此,在所有20亿左右的正常单精度值中忽略次正规和溢出,恰好有三个值x,上述代码不起作用. (注意:我原本以为只有一个;感谢@RickRegan指出评论中的错误.)所以这是我们的(略带舌头)固定代码:
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,we should never get here.
raise RuntimeError("We should never get here")