web-dev-qa-db-fra.com

Conversion de float en chaîne sans notation scientifique ni fausse précision

Je veux imprimer des nombres à virgule flottante de sorte qu'ils soient toujours écrits sous forme décimale (par exemple, 12345000000000000000000.0 Ou 0.000000000000012345, Pas notation scientifique , mais je vouloir garder les 15,7 décimales de précision et pas plus.

Il est bien connu que le 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 à nouveau en notation scientifique:

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

Il a été suggéré que je puisse utiliser format avec le drapeau f et une précision suffisante pour supprimer la notation scientifique:

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

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

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

Et si mon numéro est 4.5678e-20, Utiliser .20f Perdrait encore la précision relative:

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

Ainsi ces approches ne correspondent pas à mes besoins .


Cela conduit à la question suivante: quel est le moyen le plus simple et le plus performant d’imprimer des nombres à virgule flottante arbitraires au format décimal, avec les mêmes chiffres que dans repr(n) (ou str(n) on Python 3) , mais en utilisant toujours le format décimal, pas la notation scientifique.

En d'autres termes, une fonction ou une opération qui convertit par exemple la valeur flottante 0.00000005 En chaîne '0.00000005'; 0.1 À '0.1'; 420000000000000000.0 À '420000000000000000.0' Ou 420000000000000000 Et formate la valeur flottante -4.5678e-5 En tant que '-0.000045678'.


Après la période de prime: Il semble qu’il existe au moins deux approches viables, comme Karin a démontré qu’en manipulant des chaînes de caractères, on peut obtenir une augmentation significative de la vitesse par rapport à mon algorithme initial sur Python 2.

Ainsi,

Puisque je développe principalement sur Python 3, j'accepterai ma propre réponse et attribuerai la prime à Karin.

48
Antti Haapala

Malheureusement, il semble que même le formatage de style nouveau avec float.__format__ Ne le supporte pas. Le formatage par défaut de floats est identique à celui de repr; et avec f, il y a 6 chiffres fractionnaires par défaut:

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

Cependant, il y a un bidouillage pour obtenir le résultat souhaité - pas le plus rapide, mais relativement simple:

  • d'abord, le float est converti en chaîne à l'aide de str() ou repr()
  • alors une nouvelle instance Decimal est créée à partir de cette chaîne.
  • Decimal.__format__ Supporte f flag qui donne le résultat souhaité et, contrairement à floats, affiche la précision réelle au lieu de la précision par défaut.

Ainsi, nous pouvons créer une fonction utilitaire 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 décimal global afin de créer un nouveau contexte pour cette fonction. C'est le moyen le plus rapide. Une autre solution consisterait à utiliser decimal.local_context, mais ce serait plus lent, en créant un nouveau contexte de thread-local et un gestionnaire de contexte pour chaque conversion.

Cette fonction retourne maintenant la chaîne avec tous les chiffres possibles de mantisse, arrondis à la valeur représentation équivalente la plus courte :

>>> 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 exactement au format attendu; il retourne 420000000000000000 sans terminer .0.

36
Antti Haapala

Si vous êtes satisfait de la précision de la notation scientifique, pourrions-nous simplement adopter une approche de manipulation de chaîne simple? Ce n’est peut-être pas très intelligent, mais cela semble fonctionner (passe tous les cas d’utilisation que vous avez présentés), et je pense que cela 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')

Performance:

J'avais peur que cette approche soit trop lente, alors j'ai lancé timeit et comparé à la solution de l'OP du contexte décimal. Il semble que la manipulation des chaînes est en réalité un peu plus rapide. Edit: Il semble que ce soit beaucoup plus rapide dans Python 2. Dans Python 3, les résultats étaient similaires, mais avec le approche décimale légèrement plus rapide.

Résultat:

  • Python 2: en utilisant ctx.create_decimal(): 2.43655490875

  • Python 2: utilisation de la manipulation de chaîne: 0.305557966232

  • Python 3: en utilisant ctx.create_decimal(): 0.19519368198234588

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

Voici le code de chronométrage:

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))
25
Karin

Depuis NumPy 1.14.0, vous pouvez simplement utiliser numpy.format_float_positional . Par exemple, en vous servant des entrées de votre question:

>>> 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 utilise l'algorithme Dragon4 pour produire la représentation décimale la plus courte au format positionnel qui va-et-vient à l'entrée float d'origine. Il y a aussi numpy.format_float_scientific pour la notation scientifique, et les deux fonctions offrent des arguments optionnels pour personnaliser des éléments tels que l’arrondi et le rognage des zéros.

5
user2357112

Si vous êtes prêt à perdre votre précision de manière arbitraire en appelant str() sur le nombre à virgule flottante, alors 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 choisie comme limite supérieure pour str(float) longueur. Le supremum actuel est beaucoup plus bas. La partie or '0' Concerne la situation avec des nombres faibles et une précision nulle.

Notez que cela a toujours ses conséquences:

>> float_to_string(0.10101010101010101010101010101)
'0.10101010101'

Sinon, si la précision est importante, format est parfait:

import decimal

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

La précision perdue lors de l'appel de str(f) ne manque pas. Le 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'

Quoi qu'il en soit, le nombre maximum de décimales est limité, car le type float a lui-même ses limites et ne peut pas exprimer de flottants très 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'
3
gukoff

Je pense que rstrip peut faire le travail.

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
silgon

Question intéressante, pour ajouter un peu plus de contenu à la question, voici un petit test comparant les résultats des solutions @Antti Haapala et @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)

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

  • Avec Anti, vous verrez des chaînes comme '-000' ou '000'
  • Avec Harolds, vous verrez des chaînes comme ''

Je préférerais la cohérence même si je sacrifie un peu de vitesse. Dépend des compromis que vous souhaitez assumer pour votre cas d'utilisation.

0
BPL