Redondear decimales y enteros en Python con «round» y «Decimal.quantize

Negocio

A continuación se explica cómo redondear números en Python redondeando o redondeando a un número par. Se supone que los números son de tipo float de coma flotante o int de tipo entero.

  • función integrada (por ejemplo, en el lenguaje de programación): round()
    • Redondea los decimales a cualquier número de dígitos.
    • Redondea los enteros a cualquier número de dígitos.
    • round() redondea a un número par, no a un redondeo común
  • biblioteca estándardecimal quantize()
    • DecimalCreación de un objeto
    • Redondeo de decimales a cualquier número de cifras y redondeo a números pares
    • Redondeo de números enteros a cualquier número de cifras y redondeo a números pares
  • Definir una nueva función
    • Redondea los decimales a cualquier número de dígitos.
    • Redondear números enteros a cualquier número de dígitos
    • Nota: Para los valores negativos

Tenga en cuenta que, como se mencionó anteriormente, la función incorporada redondear no es un redondeo general, sino un redondeo a un número par. Véase más abajo para más detalles.

función integrada (por ejemplo, en el lenguaje de programación): round()

Round() se proporciona como una función integrada. Puede utilizarse sin importar ningún módulo.

El primer argumento es el número original, y el segundo es el número de dígitos (a cuántos dígitos se redondea).

Redondea los decimales a cualquier número de dígitos.

A continuación se muestra un ejemplo de procesamiento para el tipo float de punto flotante.

Si se omite el segundo argumento, se redondea a un entero. El tipo también se convierte en un entero de tipo int.

f = 123.456

print(round(f))
# 123

print(type(round(f)))
# <class 'int'>

Si se especifica el segundo argumento, devuelve un tipo de flotador de punto flotante.

Si se especifica un entero positivo, se especifica el lugar decimal; si se especifica un entero negativo, se especifica el lugar entero. -1 redondea a la décima más cercana, -2 redondea a la centésima más cercana, y 0 redondea a un entero (el primer lugar), pero devuelve un tipo flotante, a diferencia de cuando se omite.

print(round(f, 1))
# 123.5

print(round(f, 2))
# 123.46

print(round(f, -1))
# 120.0

print(round(f, -2))
# 100.0

print(round(f, 0))
# 123.0

print(type(round(f, 0)))
# <class 'float'>

Redondea los enteros a cualquier número de dígitos.

El siguiente es un ejemplo de procesamiento para el tipo entero int.

Si se omite el segundo argumento, o si se especifica 0 o un entero positivo, el valor original se devuelve tal cual. Si se especifica un entero negativo, se redondea al dígito entero correspondiente. En ambos casos, se devuelve un entero de tipo int.

i = 99518

print(round(i))
# 99518

print(round(i, 2))
# 99518

print(round(i, -1))
# 99520

print(round(i, -2))
# 99500

print(round(i, -3))
# 100000

round() redondea a un número par, no a un redondeo común

Tenga en cuenta que el redondeo con la función incorporada round() en Python 3 redondea a un número par, no a un redondeo general.

Según la documentación oficial, 0,5 se redondea a 0, 5 se redondea a 0, y así sucesivamente.

print('0.4 =>', round(0.4))
print('0.5 =>', round(0.5))
print('0.6 =>', round(0.6))
# 0.4 => 0
# 0.5 => 0
# 0.6 => 1

print('4 =>', round(4, -1))
print('5 =>', round(5, -1))
print('6 =>', round(6, -1))
# 4 => 0
# 5 => 0
# 6 => 10

La definición de redondeo a un número par es la siguiente.

Si la fracción es menor que 0,5, redondea hacia abajo; si la fracción es mayor que 0,5, redondea hacia arriba; si la fracción es exactamente 0,5, redondea al número par entre el redondeo hacia abajo y el redondeo hacia arriba.
Rounding – Wikipedia

0,5 no siempre se trunca.

print('0.5 =>', round(0.5))
print('1.5 =>', round(1.5))
print('2.5 =>', round(2.5))
print('3.5 =>', round(3.5))
print('4.5 =>', round(4.5))
# 0.5 => 0
# 1.5 => 2
# 2.5 => 2
# 3.5 => 4
# 4.5 => 4

En algunos casos, la definición de redondeo a un número par ni siquiera se aplica al procesamiento después de dos decimales.

print('0.05 =>', round(0.05, 1))
print('0.15 =>', round(0.15, 1))
print('0.25 =>', round(0.25, 1))
print('0.35 =>', round(0.35, 1))
print('0.45 =>', round(0.45, 1))
# 0.05 => 0.1
# 0.15 => 0.1
# 0.25 => 0.2
# 0.35 => 0.3
# 0.45 => 0.5

Esto se debe al hecho de que los decimales no pueden representarse exactamente como números en coma flotante, como se indica en la documentación oficial.

El comportamiento de round() para los números de coma flotante puede sorprenderle:Por ejemplo, redondear(2,675, 2) le dará 2,67 en lugar de 2,68 como se esperaba. Esto no es un error.:Esto se debe a que la mayoría de los decimales no pueden representarse exactamente con números de coma flotante.
round() — Built-in Functions — Python 3.10.2 Documentation

Si quiere conseguir un redondeo general o un redondeo preciso de los decimales a números pares, puede utilizar la cuantificación decimal de la biblioteca estándar (descrita a continuación) o definir una nueva función.

También hay que tener en cuenta que round() en Python 2 no redondea a un número par, sino que redondea.

quantize() de la biblioteca estándar decimal

El módulo decimal de la biblioteca estándar puede utilizarse para manejar números decimales exactos en coma flotante.

Utilizando el método quantize() del módulo decimal, es posible redondear los números especificando el modo de redondeo.

Los valores establecidos para el argumento redondeo del método quantize() tienen los siguientes significados, respectivamente.

  • ROUND_HALF_UP:Redondeo general
  • ROUND_HALF_EVEN:Redondeo a números pares

El módulo decimal es una biblioteca estándar, por lo que no se requiere ninguna instalación adicional, pero es necesario importarlo.

from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN

Creación de un objeto Decimal

Decimal() puede utilizarse para crear objetos de tipo Decimal.

Si se especifica un tipo de flotador como argumento, se puede ver cómo se trata realmente el valor.

print(Decimal(0.05))
# 0.05000000000000000277555756156289135105907917022705078125

print(type(Decimal(0.05)))
# <class 'decimal.Decimal'>

Como se muestra en el ejemplo, 0,05 no se trata exactamente como 0,05. Esta es la razón por la que la función incorporada round() descrita anteriormente redondea a un valor diferente del esperado para los valores decimales que incluyen 0,05 en el ejemplo.

Como 0,5 es la mitad (-1 potencia de 2), se puede expresar exactamente en notación binaria.

print(Decimal(0.5))
# 0.5

Si se especifica el tipo de cadena str en lugar del tipo float, se tratará como el tipo Decimal del valor exacto.

print(Decimal('0.05'))
# 0.05

Redondeo de decimales a cualquier número de cifras y redondeo a números pares

Llama a quantize() desde un objeto de tipo Decimal para redondear el valor.

El primer argumento de quantize() es una cadena con el mismo número de dígitos que el número de dígitos que desea encontrar, como «0.1» o «0.01».

Además, el argumento ROUNDING especifica el modo de redondeo; si se especifica ROUND_HALF_UP, se utiliza el redondeo general.

f = 123.456

print(Decimal(str(f)).quantize(Decimal('0'), rounding=ROUND_HALF_UP))
# 123

print(Decimal(str(f)).quantize(Decimal('0.1'), rounding=ROUND_HALF_UP))
# 123.5

print(Decimal(str(f)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# 123.46

A diferencia de la función incorporada round(), 0,5 se redondea a 1.

print('0.4 =>', Decimal(str(0.4)).quantize(Decimal('0'), rounding=ROUND_HALF_UP))
print('0.5 =>', Decimal(str(0.5)).quantize(Decimal('0'), rounding=ROUND_HALF_UP))
print('0.6 =>', Decimal(str(0.6)).quantize(Decimal('0'), rounding=ROUND_HALF_UP))
# 0.4 => 0
# 0.5 => 1
# 0.6 => 1

Si el argumento redondeo se establece como ROUND_HALF_EVEN, el redondeo se realiza a números pares como en la función incorporada round().

Como se ha mencionado anteriormente, si se especifica un tipo float de punto flotante como argumento de Decimal(), se trata como un objeto Decimal con un valor igual al valor real del tipo float, por lo que el resultado de utilizar el método quantize() será diferente al esperado, al igual que la función incorporada round().

print('0.05 =>', round(0.05, 1))
print('0.15 =>', round(0.15, 1))
print('0.25 =>', round(0.25, 1))
print('0.35 =>', round(0.35, 1))
print('0.45 =>', round(0.45, 1))
# 0.05 => 0.1
# 0.15 => 0.1
# 0.25 => 0.2
# 0.35 => 0.3
# 0.45 => 0.5

print('0.05 =>', Decimal(0.05).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.15 =>', Decimal(0.15).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.25 =>', Decimal(0.25).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.35 =>', Decimal(0.35).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.45 =>', Decimal(0.45).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
# 0.05 => 0.1
# 0.15 => 0.1
# 0.25 => 0.2
# 0.35 => 0.3
# 0.45 => 0.5

Si el argumento de Decimal() se especifica como una cadena de tipo str, se trata como un objeto Decimal de exactamente ese valor, por lo que el resultado es el esperado.

print('0.05 =>', Decimal(str(0.05)).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.15 =>', Decimal(str(0.15)).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.25 =>', Decimal(str(0.25)).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.35 =>', Decimal(str(0.35)).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
print('0.45 =>', Decimal(str(0.45)).quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN))
# 0.05 => 0.0
# 0.15 => 0.2
# 0.25 => 0.2
# 0.35 => 0.4
# 0.45 => 0.4

Dado que 0,5 puede ser manejado correctamente por el tipo float, no hay problema en especificar el tipo float como argumento de Decimal() cuando se redondea a un entero, pero es más seguro especificar el tipo string str cuando se redondea a un decimal.

Por ejemplo, 2,675 es en realidad 2,67499…. en tipo float. Por lo tanto, si desea redondear a dos decimales, debe especificar una cadena a Decimal(), de lo contrario el resultado será diferente al esperado tanto si se redondea al número entero más cercano (ROUND_HALF_UP) como a un número par (ROUND_HALF_EVEN).

print(Decimal(2.675))
# 2.67499999999999982236431605997495353221893310546875

print(Decimal(2.675).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# 2.67

print(Decimal(str(2.675)).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP))
# 2.68

print(Decimal(2.675).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN))
# 2.67

print(Decimal(str(2.675)).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN))
# 2.68

Tenga en cuenta que el método quantize() devuelve un número de tipo Decimal, por lo que si desea operar con un número de tipo float, debe convertirlo a un tipo float utilizando float(), de lo contrario se producirá un error.

d = Decimal('123.456').quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)

print(d)
# 123.46

print(type(d))
# <class 'decimal.Decimal'>

# print(1.2 + d)
# TypeError: unsupported operand type(s) for +: 'float' and 'decimal.Decimal'

print(1.2 + float(d))
# 124.66

Redondeo de números enteros a cualquier número de cifras y redondeo a números pares

Si quiere redondear a un dígito entero, especificar algo como '10' como primer argumento no le dará el resultado deseado.

i = 99518

print(Decimal(i).quantize(Decimal('10'), rounding=ROUND_HALF_UP))
# 99518

Esto se debe a que quantize() realiza el redondeo según el exponente del objeto Decimal, pero el exponente de Decimal('10') es 0, no 1.

Se puede especificar un exponente arbitrario utilizando E como cadena de exponente (por ejemplo, '1E1'). El exponente se puede comprobar en el método as_tuple.

print(Decimal('10').as_tuple())
# DecimalTuple(sign=0, digits=(1, 0), exponent=0)

print(Decimal('1E1').as_tuple())
# DecimalTuple(sign=0, digits=(1,), exponent=1)

Tal y como está, el resultado estará en notación exponencial usando E. Si quieres usar la notación normal, o si quieres operar con el tipo int entero después de redondear, usa int() para convertir el resultado.

print(Decimal(i).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP))
# 9.952E+4

print(int(Decimal(i).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
# 99520

print(int(Decimal(i).quantize(Decimal('1E2'), rounding=ROUND_HALF_UP)))
# 99500

print(int(Decimal(i).quantize(Decimal('1E3'), rounding=ROUND_HALF_UP)))
# 100000

Si el argumento redondeo se establece como ROUND_HALF_UP, se producirá un redondeo general, por ejemplo, 5 se redondeará a 10.

print('4 =>', int(Decimal(4).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
print('5 =>', int(Decimal(5).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
print('6 =>', int(Decimal(6).quantize(Decimal('1E1'), rounding=ROUND_HALF_UP)))
# 4 => 0
# 5 => 10
# 6 => 10

Por supuesto, no hay ningún problema si se especifica como una cadena.

Definir una nueva función

El método de utilizar el módulo decimal es preciso y seguro, pero si no se siente cómodo con la conversión de tipos, puede definir una nueva función para lograr el redondeo general.

Hay muchas formas posibles de hacerlo, por ejemplo, la siguiente función.

def my_round(val, digit=0):
    p = 10 ** digit
    return (val * p * 2 + 1) // 2 / p

Si no necesita especificar el número de dígitos y redondear siempre al primer decimal, puede utilizar una forma más sencilla.

my_round_int = lambda x: int((x * 2 + 1) // 2)

Si necesita ser preciso, es más seguro utilizar el decimal.

Lo siguiente es sólo una referencia.

Redondea los decimales a cualquier número de dígitos.

print(int(my_round(f)))
# 123

print(my_round_int(f))
# 123

print(my_round(f, 1))
# 123.5

print(my_round(f, 2))
# 123.46

A diferencia del redondeo, 0,5 se convierte en 1 según el redondeo general.

print(int(my_round(0.4)))
print(int(my_round(0.5)))
print(int(my_round(0.6)))
# 0
# 1
# 1

Redondear números enteros a cualquier número de dígitos

i = 99518

print(int(my_round(i, -1)))
# 99520

print(int(my_round(i, -2)))
# 99500

print(int(my_round(i, -3)))
# 100000

A diferencia del redondeo, 5 se convierte en 10 según el redondeo común.

print(int(my_round(4, -1)))
print(int(my_round(5, -1)))
print(int(my_round(6, -1)))
# 0
# 10
# 10

Nota: Para los valores negativos

En la función de ejemplo anterior, -0,5 se redondea a 0.

print(int(my_round(-0.4)))
print(int(my_round(-0.5)))
print(int(my_round(-0.6)))
# 0
# 0
# -1

Hay varias formas de pensar en el redondeo para los valores negativos, pero si quieres convertir -0,5 en -1, puedes modificarlo de la siguiente manera, por ejemplo

import math

def my_round2(val, digit=0):
    p = 10 ** digit
    s = math.copysign(1, val)
    return (s * val * p * 2 + 1) // 2 / p * s

print(int(my_round2(-0.4)))
print(int(my_round2(-0.5)))
print(int(my_round2(-0.6)))
# 0
# -1
# -1