Je souhaite imprimer des nombres à virgule flottante afin qu'ils soient toujours écrits sous forme décimale (par exemple 12345000000000000000000.0 ou 0.000000000000012345, pas dans notation scientifique, mais je voudrais que le résultat ait jusqu'à ~ 15,7 chiffres significatifs d'un double IEEE 754, et pas plus.

Ce que je veux, c'est idéalement pour que le résultat soit la chaîne la plus courte au format décimal positionnel qui donne toujours la même valeur lors de la conversion en float .

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 l'indicateur f 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 ait quelques zéros de fin supplémentaires. Mais alors le même format échoue pour .1, ce qui donne des chiffres décimaux au-delà de la précision réelle de float de la machine:

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

Et si mon numéro est 4.5678e-20, utiliser .20f perdrait quand même une précision relative:

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

Ainsi, ces approches ne correspondent pas à mes exigences .


Cela conduit à la question: quelle est la façon la plus simple et la plus performante d'imprimer un nombre à virgule flottante arbitraire au format décimal, avec 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-à-dire, une fonction ou une opération qui, par exemple, convertit 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 '-0.000045678'.


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

Donc,

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

68
Antti Haapala 9 août 2016 à 13:01

6 réponses

Meilleure réponse

Malheureusement, il semble que même le formatage de nouveau style avec float.__format__ ne le supporte pas. Le formatage par défaut des float est le même qu'avec repr; et avec l'indicateur f, il y a 6 chiffres fractionnaires par défaut:

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

Cependant, il existe un hack pour obtenir le résultat souhaité - pas le plus rapide, mais relativement simple:

  • le flottant est d'abord converti en chaîne en utilisant str() ou repr()
  • puis une nouvelle Decimal instance est créée à partir de cette chaîne.
  • Decimal.__format__ prend en charge l'indicateur f qui donne le résultat souhaité et, contrairement aux float, il imprime la précision réelle au lieu de la précision par défaut.

On peut donc faire une simple fonction utilitaire 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 faire attention à ne pas utiliser le contexte décimal global, donc un nouveau contexte est construit pour cette fonction. C'est le moyen le plus rapide; une autre façon serait d'utiliser decimal.local_context mais ce serait plus lent, créant un nouveau contexte de thread local et un gestionnaire de contexte pour chaque conversion.

Cette fonction renvoie désormais la chaîne avec tous les chiffres possibles de la mantisse, arrondis à la 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 l'a noté @Karin, float_to_str(420000000000000000.0) ne correspond pas strictement au format attendu; il renvoie 420000000000000000 sans suivi .0.

45
Community 23 mai 2017 à 12:16

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.

2
silgon 18 août 2016 à 08:04

Depuis NumPy 1.14.0, vous pouvez simplement utiliser {{X0} }. Par exemple, en fonction 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 retourne à l'entrée flottante d'origine. Il y a aussi numpy.format_float_scientific pour la notation scientifique, et les deux fonctions offrent des arguments facultatifs pour personnaliser des choses comme l'arrondi et le rognage des zéros.

16
user2357112 supports Monica 22 mars 2019 à 21:07

Question intéressante, pour ajouter un peu plus de contenu à la question, voici un petit test comparant les sorties 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)

Aucun d'eux 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.

2
BPL 17 août 2016 à 15:27

Si vous êtes prêt à perdre arbitrairement votre précision en appelant str() sur le nombre flottant, alors c'est la voie à suivre:

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 de 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 la longueur str(float). Le supremum réel est beaucoup plus bas. La partie or '0' est destinée à la situation avec de petits nombres et une précision nulle.

Notez qu'il a encore ses conséquences:

>> float_to_string(0.10101010101010101010101010101)
'0.10101010101'

Sinon, si la précision est importante, format 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 lors de l'appel de str(f). 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, les décimales maximales sont limitées, car le type float lui-même a ses limites et ne peut pas exprimer de flottants 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'
5
gukoff 18 août 2016 à 05:06

Si vous êtes satisfait de la précision de la notation scientifique, alors pourrions-nous simplement adopter une approche de manipulation de chaîne simple? Peut-être que ce n'est pas terriblement intelligent, mais cela semble fonctionner (passe tous les cas d'utilisation que vous avez présentés), 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, j'ai donc exécuté timeit et comparé avec la solution OP des contextes décimaux. Il semble que la manipulation des chaînes soit en fait un peu plus rapide. Modifier : il semble que ce soit beaucoup plus rapide en Python 2. En Python 3, les résultats étaient similaires, mais avec l'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înes: 0.305557966232

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

  • Python 3: utiliser la manipulation de chaînes: 0.2661344590014778

Voici le code temporel:

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))
32
Karin 19 août 2016 à 01:45