Конвертировать float в string в позиционном формате (без научной нотации и ложной точности)
Я хочу напечатать некоторые числа с плавающей запятой, чтобы они всегда записывались в десятичной форме (например, 12345000000000000000000.0
или 0.000000000000012345
, а не в научной нотации, но я бы хотел, чтобы результат имел до ~ 15,7 значащих цифр в IEEE 754 в два раза и не более.
В идеале мне нужно , чтобы в результате была получена самая короткая строка в позиционном десятичном формате, которая при преобразовании в float
все равно приводит к тому же значению.
Хорошо известно, что repr
float
записывается в научной записи, если показатель степени больше 15 или меньше -4:
>>> n = 0.000000054321654321
>>> n
5.4321654321e-08 # scientific notation
Если используется str
, результирующая строка снова находится в научной записи:
>>> str(n)
'5.4321654321e-08'
Было предложено использовать format
с флагом f
и достаточной точностью, чтобы избавиться от научной нотации:
>>> format(0.00000005, '.20f')
'0.00000005000000000000'
Это работает для этого числа, хотя у него есть некоторые дополнительные конечные нули. Но тогда тот же формат не работает для .1
, который дает десятичные цифры сверх фактической точности вычислений с плавающей точкой:
>>> format(0.1, '.20f')
'0.10000000000000000555'
И если мой номер 4.5678e-20
, использование .20f
все равно потеряет относительную точность:
>>> format(4.5678e-20, '.20f')
'0.00000000000000000005'
Таким образом эти подходы не соответствуют моим требованиям.
Это приводит к вопросу: каков самый простой и эффективный способ печати произвольного числа с плавающей запятой в десятичном формате, имеющий те же цифры, что и в repr(n)
(или str(n)
в Python 3), но всегда используя десятичный формат, а не научную запись.
То есть функция или операция, которая, например, преобразует значение с плавающей запятой 0.00000005
в строку '0.00000005'
; С 0.1
по '0.1'
; 420000000000000000.0
- '420000000000000000.0'
или 420000000000000000
и форматирует значение с плавающей точкой -4.5678e-5
как '-0.000045678'
.
После периода вознаграждения: кажется, что есть по крайней мере 2 жизнеспособных подхода, поскольку Карин продемонстрировала, что с помощью манипуляции со строками можно добиться значительного увеличения скорости по сравнению с моим первоначальным алгоритмом на Python 2.
Таким образом,
Поскольку я в основном занимаюсь разработкой на Python 3, я приму собственный ответ и награду Карин за награду.
Ответы
Ответ 1
К сожалению, кажется, что даже форматирование нового стиля с 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
.
Ответ 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
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
и сравнил его с решением десятичного десятичного контекста. Похоже, что манипуляция строк на самом деле происходит довольно быстро. Изменить. В Python 2 это намного быстрее. В Python 3 результаты были похожи, но с десятичным приближением несколько быстрее.
Результат
-
Python 2: использование ctx.create_decimal()
: 2.43655490875
-
Python 2: использование строковых манипуляций: 0.305557966232
-
Python 3: использование ctx.create_decimal()
: 0.19519368198234588
-
Python 3: использование строковых манипуляций: 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))
Ответ 3
Начиная с 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
использует алгоритм Dragon4 для получения кратчайшего десятичного представления в позиционном формате, которое возвращает обратно к исходному вводу с плавающей точкой. Также имеется numpy.format_float_scientific
для научной нотации, и обе функции предлагают необязательные аргументы для настройки таких вещей, как округление и усечение нулей.
Ответ 4
Если вы готовы потерять произвольную точность, вызывая 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'
Ответ 5
Интересный вопрос, чтобы добавить немного больше контента к вопросу, здесь тест litte, сравнивающий выходы @Antti Haapala и @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)
Ни один из них не дает "согласованных" результатов для всех случаев.
- С Anti вы увидите строки типа "-000" или "000"
- С Гарольдами вы увидите строки типа ''
Я бы предпочел бы согласованность, даже если я жертвую небольшой скоростью. Зависит от того, какие компромиссы вы хотите принять для своего случая использования.
Ответ 6
Я думаю, что rstrip
может выполнить задание.
a=5.4321654321e-08
'{0:.40f}'.format(a).rstrip("0") # float number and delete the zeros on the right
# '0.0000000543216543210000004442039220863003' # there roundoff error though
Сообщите мне, если это сработает для вас.