Conversion flottant en chaîne sans notation scientifique et fausse précision

je veux imprimer quelques nombres à virgule flottante pour qu'ils soient toujours écrits sous forme décimale (par exemple 12345000000000000000000.0 ou 0.000000000000012345, pas notation scientifique, pourtant je voudrais garder les 15,7 décimales de précision et pas plus.

Il est bien connu que l' repr d'un float est écrit en notation scientifique si l'exposant est supérieur à 15, ou inférieur à -4:

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

Si str est utilisé, la chaîne résultante est de nouveau en scientifique notation:

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

Il a été suggéré que je peux utiliser formatf drapeau et une précision suffisante pour se débarrasser de la notation scientifique:

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

cela fonctionne pour ce nombre, bien qu'il y ait quelques zéros supplémentaires. Mais alors le même format échoue pour .1, qui donne des chiffres décimaux au-delà de la précision réelle de la machine de float:

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

et si mon numéro est 4.5678e-20, en utilisant .20f perdrait encore la précision relative:

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

ainsi ces approches ne correspond pas à mes besoins.


cela nous amène à la question: Quelle est la façon la plus facile et aussi la plus performante d'imprimer un nombre de virgule flottante arbitraire en format décimal, ayant les mêmes chiffres que dans repr(n) (ou str(n) sur Python 3), mais toujours en utilisant le format décimal, pas la notation scientifique.

C'est, une fonction ou une opération que, par exemple, convertit la valeur float 0.00000005 string '0.00000005';0.1'0.1';420000000000000000.0'420000000000000000.0' ou 420000000000000000 et formate la valeur float -4.5678e-5'-0.000045678'.


après la période bounty: il semble qu'il y ait au moins 2 approches viables, car Karin a démontré qu'en utilisant la manipulation des cordes on peut obtenir une augmentation significative de la vitesse par rapport à mon algorithme initial sur Python 2.

ainsi,

puisque je me développe principalement sur Python 3, j'accepterai ma propre réponse, et j'accorderai à Karin Le bounty.

36
demandé sur Community 2016-08-09 13:01:59

5 réponses

malheureusement, il semble que même le nouveau formatage avec float.__format__ prend en charge cette. La mise en forme par défaut de floats est le même que repr; et f drapeau il y a 6 chiffres fractionnaires par défaut:

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

cependant il y a un hack pour obtenir le résultat désiré - pas le plus rapide, mais relativement simple:

  • tout d'abord le float est converti en chaîne en utilisant str() ou repr()
  • puis un nouveau Decimal l'instance est créée à partir de cette chaîne.
  • Decimal.__format__f drapeau qui donne le résultat désiré, et, contrairement à floats il affiche la précision réelle au lieu de la précision par défaut.

ainsi nous pouvons faire une fonction d'utilité simple 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')

il faut prendre soin de ne pas utiliser le contexte global décimal, donc un nouveau contexte est construit pour cette fonction. C'est la voie la plus rapide; une autre voie serait être d'utiliser decimal.local_context mais ce serait plus lent, créant un nouveau contexte thread-local et un gestionnaire de contexte pour chaque conversion.

cette fonction renvoie maintenant la chaîne avec tous les chiffres possibles de mantissa, arrondis à la plus brefs équivalent représentation:

>>> 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'

Le dernier résultat est arrondi au dernier chiffre

comme @Karin l'a noté,float_to_str(420000000000000000.0) ne correspond pas strictement au format attendu; il retourne 420000000000000000 sans fuite .0.

28
répondu Antti Haapala 2017-05-23 12:16:42

si vous êtes satisfait de la précision de la notation scientifique, alors pourrions-nous simplement prendre une approche de manipulation de chaîne simple? C'est peut-être pas très intelligent, mais il semble fonctionner (passe tous les cas d'utilisation que vous avez présenté), et je pense que c'est assez compréhensible:

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')

Performances:

j'avais peur que cette approche soit trop lente, alors j'ai couru timeit et comparé à la solution de contextes décimaux de L'OP. Il apparaît la chaîne la manipulation est en fait un peu plus rapide. Modifier: il semble être seulement beaucoup plus rapide en Python 2. Dans Python 3, les résultats étaient similaires, mais avec l'approche décimale légèrement plus rapide.

Résultat:

  • Python 2: à l'aide de ctx.create_decimal():2.43655490875

  • Python 2: à l'aide de la manipulation de la chaîne: 0.305557966232

  • Python 3: à l'aide de ctx.create_decimal(): 0.19519368198234588

  • Python 3: utilisation de la manipulation de la chaîne: 0.2661344590014778

voici le code de synchronisation:

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))
19
répondu Karin 2016-08-19 01:45:05

Si vous êtes prêt à perdre votre précision arbitraire en appelant str() sur le nombre à virgule, puis c'est le chemin à parcourir:

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'

il n'inclut pas les variables globales et vous permet de choisir vous-même la précision. La précision décimale 100 est choisi comme limite supérieure pour str(float) longueur. La suprématie réelle est beaucoup plus basse. or '0' la partie est pour la situation avec de petits nombres et la précision zéro.

notez qu'il a toujours son conséquences:

>> float_to_string(0.10101010101010101010101010101)
'0.10101010101'

Sinon, si la précision est importante, format c'est très bien:

import decimal

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

il ne manque pas la précision perdue en appelant 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'

en tout cas, les décimales maximum sont limitées, puisque le float le type lui-même a ses limites et ne peut pas exprimer des flotteurs vraiment longs:

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

de plus, les nombres entiers sont formatés tels quels.

>> float_to_string(100)
'100'
2
répondu gukoff 2016-08-18 05:06:45

question intéressante, pour ajouter un peu plus de contenu à la question, voici un petit test comparant les sorties de @Antti Haapala et @Harold solutions:

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)

ni l'un ni l'autre ne donne de résultats "cohérents" pour tous les cas.

  • avec des Anti vous verrez des chaînes comme '-000' ou '000'
  • avec Harolds vous verrez des cordes comme"

je préfère la cohérence même si je sacrifie un peu de vitesse. Cela dépend des compromis vous voulez assumer pour votre cas d'utilisation.

0
répondu BPL 2016-08-17 15:27:39

je pense que rstrip pouvez obtenir le travail fait.

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

Laissez-moi savoir si cela fonctionne pour vous.

0
répondu silgon 2016-08-18 08:04:00