Подтвердить что ты не робот

Конвертировать 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, я приму собственный ответ и награду Карин за награду.

4b9b3361

Ответ 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

Сообщите мне, если это сработает для вас.