将 float 转换为位置格式的字符串(没有科学记数法和错误精度)

我想打印一些浮点数,这样它们总是以十进制形式(例如,12345000000000000000000.00.000000000000012345,而不是 科学记数法,然而我想要的结果是最多15.7 重要数字的 IEEE 754双精度,而不是更多。

我想要的是 理想情况下 < em > ,这样的结果是位置十进制格式的 < em > 最短 字符串,当转换为 float时仍然会得到相同的值

众所周知,如果指数大于15或小于 -4,则 floatrepr是用科学记数法写的:

>>> n = 0.000000054321654321
>>> n
5.4321654321e-08  # scientific notation

如果使用 str,得到的字符串也是科学记数法:

>>> str(n)
'5.4321654321e-08'

有人建议,我可以使用带有 f标志和足够精确度的 format来消除科学记数法:

>>> format(0.00000005, '.20f')
'0.00000005000000000000'

它适用于这个数字,虽然它有一些额外的尾随零。但同样的格式也不适用于 .1,它给出的十进制数字超出了浮点数的实际机器精度:

>>> format(0.1, '.20f')
'0.10000000000000000555'

如果我的数字是 4.5678e-20,使用 .20f仍然会失去相对的精度:

>>> format(4.5678e-20, '.20f')
'0.00000000000000000005'

这就是 这些方法不符合我的要求


这就引出了一个问题: 以十进制格式打印任意浮点数(与 ABC0(或 Python 3上的 str(n))中的浮点数具有相同的数字) ,但总是使用十进制格式,而不是科学记数法,哪种方法最简单、效果最好。

也就是说,将浮点值 0.00000005转换为字符串 '0.00000005'0.1转换为 '0.1'420000000000000000.0转换为 '420000000000000000.0'420000000000000000并将浮点值 -4.5678e-5格式化为 '-0.000045678'的函数或操作。


赏金期过后: 似乎至少有两种可行的方法,正如 Karin 展示的那样,与我在 Python 2上的初始算法相比,使用字符串操作可以获得显著的速度提升。

因此,

由于我主要是在 Python 3上开发,我将接受我自己的答案,并将奖励给 Karin。

71596 次浏览

不幸的是,即使 float.__format__的新样式格式似乎也不支持这一点。float的默认格式与 repr相同; 在 f标志中,默认有6个小数位:

>>> format(0.0000000005, 'f')
'0.000000'

然而,有一种方法可以达到预期的效果——不是最快的,但相对简单:

  • 首先,使用 str()repr()将浮点数转换为字符串
  • 然后从该字符串创建一个新的 Decimal实例。
  • Decimal.__format__支持提供所需结果的 f标志,并且与 float不同,它打印的是实际精度而不是默认精度。

因此,我们可以做一个简单的效用函数 float_to_str:

import decimal


# create a new context for this task
ctx = decimal.Context()


# 20 digits should be enough for everyone :D
ctx.prec = 20


def float_to_str(f):
"""
Convert the given float to a string,
without resorting to scientific notation
"""
d1 = ctx.create_decimal(repr(f))
return format(d1, 'f')

必须注意不要使用全局十进制上下文,因此为此函数构造了一个新的上下文。这是最快的方法; 另一种方法是使用 decimal.local_context,但它会更慢,为每次转换创建一个新的线程本地上下文和一个上下文管理器。

此函数现在返回包含尾数中所有可能数字的字符串,四舍五入到 最短等价表示法最短等价表示法:

>>> float_to_str(0.1)
'0.1'
>>> float_to_str(0.00000005)
'0.00000005'
>>> float_to_str(420000000000000000.0)
'420000000000000000'
>>> float_to_str(0.000000000123123123123123123123)
'0.00000000012312312312312313'

最后一个结果在最后一个数字处四舍五入

正如@Karin 所指出的,float_to_str(420000000000000000.0)并不严格匹配预期的格式; 它返回 420000000000000000而不跟随 .0

如果你已经准备好通过调用浮点数 str()来失去你的精度,那么这就是你要走的路:

import decimal


def float_to_string(number, precision=20):
return '{0:.{prec}f}'.format(
decimal.Context(prec=100).create_decimal(str(number)),
prec=precision,
).rstrip('0').rstrip('.') or '0'

它不包含全局变量,允许您自己选择精度。选择十进制精度100作为 str(float)长度的上界。实际的上限要低得多。or '0'部分是用于小数目和零精度的情况。

请注意,它仍有其后果:

>> float_to_string(0.10101010101010101010101010101)
'0.10101010101'

否则,如果精度很重要,那么 format就很好:

import decimal


def float_to_string(number, precision=20):
return '{0:.{prec}f}'.format(
number, prec=precision,
).rstrip('0').rstrip('.') or '0'

它不会错过调用 str(f)时所丢失的精度。 or

>> float_to_string(0.1, precision=10)
'0.1'
>> float_to_string(0.1)
'0.10000000000000000555'
>>float_to_string(0.1, precision=40)
'0.1000000000000000055511151231257827021182'


>>float_to_string(4.5678e-5)
'0.000045678'


>>float_to_string(4.5678e-5, precision=1)
'0'

无论如何,最大小数位是有限的,因为 float类型本身有其限制,不能表示真正长的浮点数:

>> float_to_string(0.1, precision=10000)
'0.1000000000000000055511151231257827021181583404541015625'

此外,整个数字也按原样格式化。

>> float_to_string(100)
'100'

如果你对科学记数法的精确度感到满意,那么我们可以采取一种简单的字符串操作方法吗?也许它不是特别聪明,但是它似乎可以工作(通过所有你提供的用例) ,我认为这是可以理解的:

def float_to_str(f):
float_string = repr(f)
if 'e' in float_string:  # detect scientific notation
digits, exp = float_string.split('e')
digits = digits.replace('.', '').replace('-', '')
exp = int(exp)
zero_padding = '0' * (abs(int(exp)) - 1)  # minus 1 for decimal point in the sci notation
sign = '-' if f < 0 else ''
if exp > 0:
float_string = '{}{}{}.0'.format(sign, digits, zero_padding)
else:
float_string = '{}0.{}{}'.format(sign, zero_padding, digits)
return float_string


n = 0.000000054321654321
assert(float_to_str(n) == '0.000000054321654321')


n = 0.00000005
assert(float_to_str(n) == '0.00000005')


n = 420000000000000000.0
assert(float_to_str(n) == '420000000000000000.0')


n = 4.5678e-5
assert(float_to_str(n) == '0.000045678')


n = 1.1
assert(float_to_str(n) == '1.1')


n = -4.5678e-5
assert(float_to_str(n) == '-0.000045678')

表现 :

我担心这种方法可能太慢,所以我运行了 timeit并与 OP 的十进制上下文解决方案进行了比较。看起来字符串操作实际上要快得多。剪辑: 在 Python2中似乎只是快得多。在 Python3中,结果类似,但是使用十进制方法稍微快一些。

结果 :

  • Python 2: using ctx.create_decimal(): 2.43655490875

  • Python2: 使用字符串操作: 0.305557966232

  • Python 3: 使用 ctx.create_decimal(): 0.19519368198234588

  • Python3: 使用字符串操作: 0.2661344590014778

这是计时码:

from timeit import timeit


CODE_TO_TIME = '''
float_to_str(0.000000054321654321)
float_to_str(0.00000005)
float_to_str(420000000000000000.0)
float_to_str(4.5678e-5)
float_to_str(1.1)
float_to_str(-0.000045678)
'''
SETUP_1 = '''
import decimal


# create a new context for this task
ctx = decimal.Context()


# 20 digits should be enough for everyone :D
ctx.prec = 20


def float_to_str(f):
"""
Convert the given float to a string,
without resorting to scientific notation
"""
d1 = ctx.create_decimal(repr(f))
return format(d1, 'f')
'''
SETUP_2 = '''
def float_to_str(f):
float_string = repr(f)
if 'e' in float_string:  # detect scientific notation
digits, exp = float_string.split('e')
digits = digits.replace('.', '').replace('-', '')
exp = int(exp)
zero_padding = '0' * (abs(int(exp)) - 1)  # minus 1 for decimal point in the sci notation
sign = '-' if f < 0 else ''
if exp > 0:
float_string = '{}{}{}.0'.format(sign, digits, zero_padding)
else:
float_string = '{}0.{}{}'.format(sign, zero_padding, digits)
return float_string
'''


print(timeit(CODE_TO_TIME, setup=SETUP_1, number=10000))
print(timeit(CODE_TO_TIME, setup=SETUP_2, number=10000))

有趣的问题,为了给这个问题添加更多的内容,这里有一个小测试,比较@AntiHaapala 和@Harold 的解决方案输出:

import decimal
import math


ctx = decimal.Context()




def f1(number, prec=20):
ctx.prec = prec
return format(ctx.create_decimal(str(number)), 'f')




def f2(number, prec=20):
return '{0:.{prec}f}'.format(
number, prec=prec,
).rstrip('0').rstrip('.')


k = 2*8


for i in range(-2**8,2**8):
if i<0:
value = -k*math.sqrt(math.sqrt(-i))
else:
value = k*math.sqrt(math.sqrt(i))


value_s = '{0:.{prec}E}'.format(value, prec=10)


n = 10


print ' | '.join([str(value), value_s])
for f in [f1, f2]:
test = [f(value, prec=p) for p in range(n)]
print '\t{0}'.format(test)

两者都没有给出所有情况下“一致”的结果。

  • 有了安替,你会看到字符串像’-000’或’000’
  • 用哈罗德的字符串,你会看到这样的字符串

即使牺牲一点点速度,我也希望保持一致性。取决于您想为您的用例假设哪些权衡。

我认为 rstrip可以完成这项工作。

a=5.4321654321e-08
'{0:.40f}'.format(a).rstrip("0") # float number and delete the zeros on the right
# '0.0000000543216543210000004442039220863003' # there's roundoff error though

如果可以的话告诉我。

从 NumPy 1.14.0开始,你可以只使用 numpy.format_float_positional:

>>> numpy.format_float_positional(0.000000054321654321)
'0.000000054321654321'
>>> numpy.format_float_positional(0.00000005)
'0.00000005'
>>> numpy.format_float_positional(0.1)
'0.1'
>>> numpy.format_float_positional(4.5678e-20)
'0.000000000000000000045678'

numpy.format_float_positional使用 Dragon 4算法生成位置格式的最短小数,从而返回到原始浮点数输入。还有用于科学记数法的 numpy.format_float_scientific函数,这两个函数都提供可选参数来定制舍入和修剪零之类的东西。

使用格式(float,’. f’) :

old = 0.00000000000000000000123
if str(old).__contains__('e-'):
float_length = str(old)[-2:]
new=format(old,'.'+str(float_length)+'f')
print(old)
print(new)