python - Convierte float en string sin notación científica y precisión falsa
python-3.x floating-point (5)
Quiero imprimir algunos números de punto flotante para que siempre estén escritos en forma decimal (por ejemplo, 12345000000000000000000.0
o 0.000000000000012345
, no en notación científica , pero me gustaría mantener los 15.7 dígitos decimales de precisión y no más).
Es bien sabido que la repr
de un float
está escrita en notación científica si el exponente es mayor que 15 o menor que -4:
>>> n = 0.000000054321654321
>>> n
5.4321654321e-08 # scientific notation
Si se usa str
, la cadena resultante nuevamente está en notación científica:
>>> str(n)
''5.4321654321e-08''
Se ha sugerido que puedo usar format
con bandera f
y suficiente precisión para eliminar la notación científica:
>>> format(0.00000005, ''.20f'')
''0.00000005000000000000''
Funciona para ese número, aunque tiene algunos ceros finales adicionales. Pero luego el mismo formato falla para .1
, que da dígitos decimales más allá de la precisión real de la máquina de flotación:
>>> format(0.1, ''.20f'')
''0.10000000000000000555''
Y si mi número es 4.5678e-20
, el uso de .20f
aún perdería una precisión relativa:
>>> format(4.5678e-20, ''.20f'')
''0.00000000000000000005''
Por lo tanto, estos enfoques no coinciden con mis requisitos .
Esto lleva a la pregunta: ¿cuál es la manera más fácil y mejor de imprimir número de punto flotante arbitrario en formato decimal, teniendo los mismos dígitos que en repr(n)
(o str(n)
en Python 3) , pero siempre usando el formato decimal, no la notación científica.
Es decir, una función u operación que, por ejemplo, convierte el valor flotante 0.00000005
en la cadena ''0.00000005''
; 0.1
a ''0.1''
; 420000000000000000.0
a ''420000000000000000.0''
o 420000000000000000
y formatea el valor flotante -4.5678e-5
como ''-0.000045678''
.
Después del período de recompensa: parece que hay al menos 2 enfoques viables, ya que Karin demostró que mediante la manipulación de cadenas se puede lograr un impulso de velocidad significativo en comparación con mi algoritmo inicial en Python 2.
Así,
- Si el rendimiento es importante y se requiere compatibilidad con Python 2; o si el módulo
decimal
no puede usarse por alguna razón, entonces el enfoque de Karin usando la manipulación de cuerdas es la manera de hacerlo. - En Python 3, mi código algo más corto también será más rápido .
Como estoy desarrollando principalmente en Python 3, aceptaré mi propia respuesta y otorgaré a Karin la recompensa.
Creo que rstrip
puede hacer el trabajo.
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
Avísame si eso funciona para ti.
Interesante pregunta, para agregar un poco más de contenido a la pregunta, aquí hay una pequeña prueba que compara los resultados de las soluciones de @Antti Haapala y @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)
Ninguno de ellos brinda resultados "consistentes" para todos los casos.
- Con Anti verás cadenas como ''-000'' o ''000''
- Con Harolds''s verá cadenas como ''''
Preferiría consistencia incluso si estoy sacrificando un poco de velocidad. Depende de las compensaciones que quiera asumir para su caso de uso.
Lamentablemente, parece que ni siquiera el formato de estilo nuevo con float.__format__
admite. El formato predeterminado de float
es el mismo que con repr
; y con f
bandera hay 6 dígitos fraccionarios por defecto:
>>> format(0.0000000005, ''f'')
''0.000000''
Sin embargo, hay un truco para obtener el resultado deseado, no el más rápido, pero relativamente simple:
- primero el flotador se convierte en una cadena usando
str()
orepr()
- luego se crea una nueva instancia de
Decimal
partir de esa cadena. -
Decimal.__format__
admitef
flag que proporciona el resultado deseado y, a diferencia defloat
s, imprime la precisión real en lugar de la predeterminada.
Por lo tanto, podemos hacer una función de utilidad 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'')
Se debe tener cuidado de no utilizar el contexto decimal global, por lo que se construye un nuevo contexto para esta función. Esta es la manera más rápida; Otra forma sería usar decimal.local_context
pero sería más lento, creando un nuevo contexto local y un administrador de contexto para cada conversión.
Esta función ahora devuelve la cadena con todos los dígitos posibles de mantissa, redondeada a la representación equivalente más corta :
>>> 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''
El último resultado se redondea en el último dígito
Como señaló @Karin, float_to_str(420000000000000000.0)
no coincide estrictamente con el formato esperado; devuelve 420000000000000000
sin trailing .0
.
Si está listo para perder su precisión arbitraria llamando a str()
en el número flotante, entonces es el camino a seguir:
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''
No incluye variables globales y le permite elegir la precisión usted mismo. La precisión decimal 100 se elige como un límite superior para la longitud str(float)
. El supremo real es mucho más bajo. La parte or ''0''
es para la situación con números pequeños y precisión cero.
Tenga en cuenta que todavía tiene sus consecuencias:
>> float_to_string(0.10101010101010101010101010101)
''0.10101010101''
De lo contrario, si la precisión es importante, el format
es correcto:
import decimal
def float_to_string(number, precision=20):
return ''{0:.{prec}f}''.format(
number, prec=precision,
).rstrip(''0'').rstrip(''.'') or ''0''
No se pierde la precisión que se pierde al llamar a str(f)
. El 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''
De todos modos, los lugares decimales máximos son limitados, ya que el tipo de float
sí tiene sus límites y no puede expresar flotantes realmente largos:
>> float_to_string(0.1, precision=10000)
''0.1000000000000000055511151231257827021181583404541015625''
Además, los números enteros se están formateando tal como están.
>> float_to_string(100)
''100''
Si está satisfecho con la precisión de la notación científica, ¿podríamos tomar un enfoque simple de manipulación de cuerdas? Tal vez no es demasiado inteligente, pero parece funcionar (pasa todos los casos de uso que ha presentado), y creo que es bastante comprensible:
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'')
Rendimiento :
Me preocupaba que este enfoque fuera demasiado lento, así que ejecuté el timeit
y lo timeit
con la solución de OP de contextos decimales. Parece que la manipulación de cadenas es bastante más rápida. Editar : parece ser mucho más rápido en Python 2. En Python 3, los resultados fueron similares, pero con el enfoque decimal ligeramente más rápido.
Resultado :
Python 2: usando
ctx.create_decimal()
:2.43655490875
Python 2: utilizando la manipulación de cadenas:
0.305557966232
Python 3: usando
ctx.create_decimal()
:0.19519368198234588
Python 3: utilizando la manipulación de cadenas:
0.2661344590014778
Aquí está el código de tiempo:
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))