Solución:
Desafortunadamente, parece que ni siquiera el formato de nuevo estilo con float.__format__
apoya esto. El formato predeterminado de float
s es lo mismo que con repr
; y con f
bandera hay 6 dígitos fraccionarios por defecto:
>>> format(0.0000000005, 'f')
'0.000000'
Sin embargo, existe 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 una nueva
Decimal
instancia se crea a partir de esa cadena. -
Decimal.__format__
apoyosf
bandera que da el resultado deseado y, a diferencia defloat
s imprime la precisión real en lugar de la precisión predeterminada.
Así 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. Ésta es la forma más rápida; otra forma sería usar decimal.local_context
pero sería más lento, creando un nuevo contexto local de hilo y un administrador de contexto para cada conversión.
Esta función ahora devuelve la cadena con todos los dígitos posibles de la mantisa, redondeados 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 al último dígito
Como señaló @Karin, float_to_str(420000000000000000.0)
no coincide estrictamente con el formato esperado; vuelve 420000000000000000
sin arrastrar .0
.
Si está satisfecho con la precisión de la notación científica, ¿podríamos simplemente adoptar un enfoque simple de manipulación de cuerdas? Tal vez no sea terriblemente 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 corrí timeit
y comparado con la solución OP de contextos decimales. Parece que la manipulación de cuerdas es bastante más rápida. Editar: Parece que solo es mucho más rápido en Python 2. En Python 3, los resultados fueron similares, pero con el enfoque decimal un poco más rápido.
Resultado:
-
Python 2: usando
ctx.create_decimal()
:2.43655490875
-
Python 2: usando manipulación de cadenas:
0.305557966232
-
Python 3: usando
ctx.create_decimal()
:0.19519368198234588
-
Python 3: usando 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))
A partir de NumPy 1.14.0, puede usar numpy.format_float_positional
. Por ejemplo, ejecutando las entradas de su pregunta:
>>> 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
utiliza el algoritmo Dragon4 para producir la representación decimal más corta en formato posicional que regresa a la entrada flotante original. También hay numpy.format_float_scientific
para notación científica, y ambas funciones ofrecen argumentos opcionales para personalizar cosas como redondeo y recorte de ceros.