您正在寻求解决与 Python 本质上相同的问题repr
解决,即找到舍入为给定浮点数的最短十进制字符串。除了在您的情况下,浮点不是 IEEE 754 二进制 64(“双精度”)浮点,而是 IEEE 754 二进制 32(“单精度”)浮点。
只是为了记录,我当然应该指出检索原始字符串表示是不可能的,因为例如字符串'0.10'
, '0.1'
, '1e-1'
and '10e-2'
全部转换为相同的浮点数(或者在本例中float32
)。但在合适的条件下,我们仍然可以希望生成一个与原始字符串具有相同十进制值的字符串,这就是我下面要做的。
您在答案中概述的方法或多或少有效,但可以稍微简化一下。
首先,一些界限:当涉及到单精度浮点数的十进制表示时,有两个幻数:6
and 9
。意义6
任何具有 6 个或更少有效十进制数字的(不太大、不太小的)十进制数字字符串都将通过单精度 IEEE 754 浮点数正确往返:即将该字符串转换为最接近的值float32
,然后转换that值返回到最接近的值6
-digit 十进制字符串,将生成一个与原始值相同的字符串。例如:
>>> x = "634278e13"
>>> y = float(np.float32(x))
>>> y
6.342780214942106e+18
>>> "{:.6g}".format(y)
'6.34278e+18'
(这里,“不太大,不太小”只是指下溢和上溢范围float32
应该避免。上述属性适用于所有正常值。)
这意味着对于您的问题,如果original字符串有 6 位或更少的数字,我们可以通过简单地将值格式化为 6 位有效数字来恢复它。因此,如果您只关心恢复具有 6 个或更少有效十进制数字的字符串,您可以停止阅读此处:一个简单的'{:.6g}'.format(x)
足够。如果您想更普遍地解决问题,请继续阅读。
对于另一个方向的往返,我们有相反的属性:给定任何单精度浮点数x
,将该浮点转换为 9 位十进制字符串(一如既往地四舍五入到最接近的值),然后将该字符串转换回单精度浮点,将始终精确地恢复该浮点的值。
>>> x = np.float32(3.14159265358979)
>>> x
3.1415927
>>> np.float32('{:.9g}'.format(x)) == x
True
与你的问题的相关性是always至少一个 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 位十进制数字字符串不再能够通过单精度浮点数正确往返。如果你关心次正常,你需要在那里做一些更复杂的事情。
第三,上面有一个非常微妙(而且有趣!)的错误:almost根本不重要。我们使用的字符串格式总是四舍五入x
to the nearest places
-digit 十进制字符串到真实值x
。然而,我们只想知道是否存在any places
-digit 十进制字符串四舍五入到x
。我们隐含地假设(看似显而易见的)事实:如果有any places
- 四舍五入到的十进制字符串x
,那么closest places
-digit 十进制字符串四舍五入为x
。那就是almosttrue:根据以下属性,舍入到的所有实数的间隔x
周围对称x
。但这种对称性在一种特殊情况下会失效,即当x
是一个幂2
.
So when x
是一个精确的幂2
, it's possible(但不太可能)(例如)最接近的 8 位十进制字符串x
doesn't舍入到x
,但仍然有一个 8 位十进制字符串可以舍入为x
。您可以对在范围内发生这种情况的情况进行详尽的搜索float32
,事实证明,恰好有三个值x
发生这种情况的情况,即x = 2**-96
, x = 2**87
and 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'
事实证明这个值doesn't绕回到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 亿左右的正正规单精度值中,正好有three values 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, 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")