python python-3.x floating-point number-formatting python-2.x

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() o repr()
  • luego se crea una nueva instancia de Decimal partir de esa cadena.
  • Decimal.__format__ admite f flag que proporciona el resultado deseado y, a diferencia de float 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))