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,
decimal
ne peut pas être utilisé pour une raison quelconque, alors l'approche de Karin utilisant la manipulation de chaîne est le moyen de le faire.Puisque je développe principalement sur Python 3, j'accepterai ma propre réponse et attribuerai la prime à Karin.
Malheureusement, il semble que même le formatage de style nouveau avec float.__format__
Ne le supporte pas. Le formatage par défaut de float
s 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:
str()
ou repr()
Decimal
est créée à partir de cette chaîne.Decimal.__format__
Supporte f
flag qui donne le résultat souhaité et, contrairement à float
s, 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
.
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))
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.
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'
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.
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.
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.