Primero, la función, para aquellos que solo quieren un código de copiar y pegar:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '{}'.format(f)
if 'e' in s or 'E' in s:
return '{0:.{1}f}'.format(f, n)
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
Esto es válido en Python 2.7 y 3.1+. Para las versiones anteriores, no es posible obtener el mismo efecto de "redondeo inteligente" (al menos, no sin mucho código complicado), pero el redondeo a 12 lugares decimales antes del truncamiento funcionará la mayor parte del tiempo:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '%.12f' % f
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
Explicación
El núcleo del método subyacente es convertir el valor en una cadena con total precisión y luego simplemente cortar todo más allá del número deseado de caracteres. El último paso es sencillo; se puede hacer con manipulación de cuerdas
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
o el decimal
modulo
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
El primer paso, convertir a una cadena, es bastante difícil porque hay algunos pares de literales de punto flotante (es decir, lo que escribe en el código fuente) que producen la misma representación binaria y, sin embargo, deben truncarse de manera diferente. Por ejemplo, considere 0.3 y 0.29999999999999998. Si escribe 0.3
en un programa Python, el compilador lo codifica utilizando el formato de punto flotante IEEE en la secuencia de bits (asumiendo un flotante de 64 bits)
0011111111010011001100110011001100110011001100110011001100110011
Este es el valor más cercano a 0.3 que se puede representar con precisión como un flotante IEEE. Pero si escribe 0.29999999999999998
en un programa de Python, el compilador lo traduce exactamente al mismo valor . En un caso, quería que se truncara (a un dígito) como 0.3
, mientras que en el otro caso, quería que se truncase como 0.2
, pero Python solo puede dar una respuesta. Esta es una limitación fundamental de Python, o de cualquier lenguaje de programación sin evaluación perezosa. La función de truncamiento solo tiene acceso al valor binario almacenado en la memoria de la computadora, no a la cadena que realmente ingresó en el código fuente. 1
Si decodifica la secuencia de bits en un número decimal, nuevamente usando el formato de punto flotante IEEE de 64 bits, obtiene
0.2999999999999999888977697537484345957637...
por lo que se produciría una implementación ingenua 0.2
, aunque probablemente eso no sea lo que desea. Para obtener más información sobre el error de representación de punto flotante, consulte el tutorial de Python .
Es muy raro trabajar con un valor de punto flotante que está tan cerca de un número redondo y, sin embargo, no es intencionalmente igual a ese número redondo. Entonces, al truncar, probablemente tenga sentido elegir la representación decimal "más bonita" de todas las que podrían corresponder al valor en la memoria. Python 2.7 y versiones posteriores (pero no 3.0) incluye un algoritmo sofisticado para hacer precisamente eso , al que podemos acceder a través de la operación de formato de cadena predeterminada.
'{}'.format(f)
La única advertencia es que esto actúa como una g
especificación de formato, en el sentido de que usa notación exponencial ( 1.23e+4
) si el número es lo suficientemente grande o pequeño. Entonces, el método tiene que detectar este caso y manejarlo de manera diferente. Hay algunos casos en los que el uso de una f
especificación de formato causa un problema, como intentar truncar 3e-10
a 28 dígitos de precisión (produce 0.0000000002999999999999999980
), y todavía no estoy seguro de cuál es la mejor manera de manejarlos.
Si realmente está trabajando con float
s que están muy cerca de los números redondeados pero intencionalmente no son iguales a ellos (como 0.29999999999999998 o 99.959999999999994), esto producirá algunos falsos positivos, es decir, redondeará los números que no desea redondear. En ese caso, la solución es especificar una precisión fija.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
El número de dígitos de precisión a usar aquí realmente no importa, solo necesita ser lo suficientemente grande para garantizar que cualquier redondeo realizado en la conversión de cadena no "aumente" el valor a su agradable representación decimal. Creo que sys.float_info.dig + n + 2
puede ser suficiente en todos los casos, pero si no es así, 2
podría ser necesario aumentarlo, y no está de más hacerlo.
En versiones anteriores de Python (hasta 2.6 o 3.0), el formato de número de punto flotante era mucho más burdo y producía regularmente cosas como
>>> 1.1
1.1000000000000001
Si ésta es su situación, si usted no desea utilizar "buenos" representaciones decimales para truncar, todo lo que puede hacer (por lo que yo sé) es elegir un número de dígitos, menos de lo representable precisión completa por una float
, y alrededor de la número a esa cantidad de dígitos antes de truncarlo. Una elección típica es 12,
'%.12f' % f
pero puedes ajustar esto para que se adapte a los números que estás usando.
1 Bueno ... mentí. Técnicamente, puede indicarle a Python que vuelva a analizar su propio código fuente y extraiga la parte correspondiente al primer argumento que pase a la función de truncamiento. Si ese argumento es un literal de punto flotante, puede cortarlo un cierto número de lugares después del punto decimal y devolverlo. Sin embargo, esta estrategia no funciona si el argumento es una variable, lo que la hace bastante inútil. Lo siguiente se presenta solo con fines de entretenimiento:
def trunc_introspect(f, n):
'''Truncates/pads the float f to n decimal places by looking at the caller's source code'''
current_frame = None
caller_frame = None
s = inspect.stack()
try:
current_frame = s[0]
caller_frame = s[1]
gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline)
for token_type, token_string, _, _, _ in gen:
if token_type == tokenize.NAME and token_string == current_frame[3]:
next(gen) # left parenthesis
token_type, token_string, _, _, _ = next(gen) # float literal
if token_type == tokenize.NUMBER:
try:
cut_point = token_string.index('.') + n + 1
except ValueError: # no decimal in string
return token_string + '.' + '0' * n
else:
if len(token_string) < cut_point:
token_string += '0' * (cut_point - len(token_string))
return token_string[:cut_point]
else:
raise ValueError('Unable to find floating-point literal (this probably means you called {} with a variable)'.format(current_frame[3]))
break
finally:
del s, current_frame, caller_frame
Generalizar esto para manejar el caso en el que pasa una variable parece una causa perdida, ya que tendría que rastrear hacia atrás a través de la ejecución del programa hasta encontrar el literal de punto flotante que le dio a la variable su valor. Si es que hay uno. La mayoría de las variables se inicializarán a partir de la entrada del usuario o expresiones matemáticas, en cuyo caso la representación binaria es todo lo que hay.