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 format
f
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,
- si la performance est importante et la compatibilité Python 2 est requise; ou si le
decimal
module ne peut pas être utilisé pour une raison quelconque, alors Karin de l'approche à l'aide de la manipulation de la chaîne est la façon de le faire. - Sur Python 3, ma un peu plus court code sera également plus rapide.
puisque je me développe principalement sur Python 3, j'accepterai ma propre réponse, et j'accorderai à Karin Le bounty.
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 float
s 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()
ourepr()
- 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 àfloat
s 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
.
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))
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'
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.
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.