Arredondamento de decimais e inteiros em Python com “redondo” e “Decimal.quantize

O negócio

O seguinte explica como arredondar números em Python por arredondamento ou arredondamento para um número par. Assume-se que os números são do tipo flutuante de ponto flutuante ou int inteiro.

  • função incorporada (por exemplo, em linguagem de programação): round()
    • Redondo decimais a qualquer número de dígitos.
    • Redondo números inteiros para qualquer número de dígitos.
    • redonda() redonda para um número par, não para um arredondamento comum
  • biblioteca padrãodecimal quantize()
    • DecimalCriação de um objecto
    • Arredondamento de decimais para qualquer número de dígitos e arredondamento para números pares
    • Arredondamento de números inteiros para qualquer número de dígitos e arredondamento para números pares
  • Definir uma nova função
    • Arredondar as casas decimais para qualquer número de dígitos.
    • Redondo números inteiros a qualquer número de dígitos
    • Nota: Para valores negativos

Note-se que, como mencionado acima, o arredondamento da função integrada não é um arredondamento geral, mas um arredondamento para um número par. Ver abaixo para detalhes.

função incorporada (por exemplo, em linguagem de programação): round()

Round() é fornecido como uma função incorporada. Pode ser utilizado sem importação de quaisquer módulos.

O primeiro argumento é o número original, e o segundo argumento é o número de dígitos (quantos dígitos arredondar para).

Redondo decimais a qualquer número de dígitos.

O seguinte é um exemplo de processamento para o tipo de flutuador de ponto flutuante.

Se o segundo argumento for omitido, é arredondado para um número inteiro. O tipo também se torna um integer int tipo.

f = 123.456

print(round(f))
# 123

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

Se o segundo argumento for especificado, devolve um tipo de flutuador de ponto flutuante.

Se for especificado um número inteiro positivo, é especificada a casa decimal; se for especificado um número inteiro negativo, é especificada a casa inteira. -1 arredondamentos para o décimo mais próximo, -2 arredondamentos para o centésimo mais próximo, e 0 arredondamentos para um número inteiro (o primeiro lugar), mas devolve um tipo de flutuador, ao contrário de quando omitido.

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'>

Redondo números inteiros para qualquer número de dígitos.

O seguinte é um exemplo de processamento para o tipo int inteiro.

Se o segundo argumento for omitido, ou se for especificado 0 ou um número inteiro positivo, o valor original é devolvido tal como está. Se for especificado um número inteiro negativo, este é arredondado para o dígito inteiro correspondente. Em ambos os casos, é devolvido um inteiro inteiro do 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

redonda() redonda para um número par, não para um arredondamento comum

Note-se que o arredondamento com a função round() incorporada em Python 3 arredondamentos para um número par, não para um arredondamento geral.

Como escrito na documentação oficial, 0,5 é arredondado para 0, 5 é arredondado para 0, e assim por diante.

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

A definição de arredondamento para um número par é a seguinte.

Se a fracção for inferior a 0,5, arredondá-la para baixo; se a fracção for superior a 0,5, arredondá-la para cima; se a fracção for exactamente 0,5, arredondá-la para cima até ao número par entre o arredondamento para baixo e o arredondamento para cima.
Rounding – Wikipedia

0,5 nem sempre é truncado.

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

Em alguns casos, a definição de arredondamento para um número par nem sequer se aplica ao processamento após duas casas decimais.

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

Isto deve-se ao facto de as casas decimais não poderem ser representadas exactamente como números de pontos flutuantes, tal como indicado na documentação oficial.

O comportamento de redondo() para números de ponto flutuante pode surpreendê-lo:Por exemplo, a ronda(2,675, 2) dar-lhe-á 2,67 em vez de 2,68 como esperado. Isto não é um bug.:Isto resulta do facto de a maioria das casas decimais não poder ser representada exactamente por números de pontos flutuantes.
round() — Built-in Functions — Python 3.10.2 Documentation

Se desejar obter arredondamentos gerais ou arredondamentos precisos de decimais para números pares, pode usar a quantificação padrão de decimais da biblioteca (descrita abaixo) ou definir uma nova função.

Note-se também que o arredondamento() em Python 2 não é um arredondamento para um número par, mas sim um arredondamento.

quantize() da biblioteca padrão decimal

O módulo decimal da biblioteca padrão pode ser utilizado para lidar com números exactos de pontos flutuantes decimais.

Usando o método quantize() do módulo decimal, é possível arredondar números especificando o modo de arredondamento.

Os valores definidos para o arredondamento do argumento do método quantize() têm os seguintes significados, respectivamente.

  • ROUND_HALF_UP:Arredondamento geral
  • ROUND_HALF_EVEN:Arredondamento para números pares

O módulo decimal é uma biblioteca padrão, pelo que não é necessária qualquer instalação adicional, mas é necessária a importação.

from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN

Criação de um objecto Decimal

Decimal() pode ser usado para criar objectos do tipo Decimal.

Se especificar um tipo de flutuador como argumento, pode ver como o valor é realmente tratado.

print(Decimal(0.05))
# 0.05000000000000000277555756156289135105907917022705078125

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

Como mostrado no exemplo, 0,05 não é tratado como exactamente 0,05. Esta é a razão pela qual a função integrada arredondada() acima descrita é arredondada para um valor diferente do esperado para valores decimais, incluindo 0,05 no exemplo.

Uma vez que 0,5 é metade (-1 potência de 2), pode ser expresso exactamente em notação binária.

print(Decimal(0.5))
# 0.5

Se especificar a string tipo str em vez do tipo float, será tratado como o tipo Decimal do valor exacto.

print(Decimal('0.05'))
# 0.05

Arredondamento de decimais para qualquer número de dígitos e arredondamento para números pares

Chamada quantize() de um objecto do tipo Decimal para arredondar o valor.

O primeiro argumento de quantize() é uma cadeia com o mesmo número de dígitos que o número de dígitos que pretende encontrar, como '0,1' ou '0,01'.

Além disso, o argumento ROUNDING especifica o modo de arredondamento; se ROUND_HALF_UP for especificado, é utilizado o arredondamento geral.

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

Ao contrário da função round() integrada, 0,5 é arredondado para 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

Se o arredondamento do argumento estiver definido para ROUND_HALF_EVEN, o arredondamento é efectuado para números pares como na função integrada round().

Como mencionado acima, se um tipo de flutuador de ponto flutuante for especificado como o argumento de Decimal(), é tratado como um objecto Decimal com um valor igual ao valor real do tipo de flutuador, pelo que o resultado da utilização do método quantize() será diferente do que se espera, tal como a função integrada 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

Se o argumento de Decimal() for especificado como uma string do tipo str, é tratado como um objecto Decimal exactamente desse valor, pelo que o resultado é o 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

Uma vez que 0,5 pode ser correctamente manipulado pelo tipo de flutuador, não há problema em especificar o tipo de flutuador como o argumento de Decimal() quando se arredonda para um número inteiro, mas é mais seguro especificar o tipo de string string quando se arredonda para uma casa decimal.

Por exemplo, 2.675 é na realidade 2.67499…. em tipo flutuante. Portanto, se quiser arredondar para duas casas decimais, deve especificar uma string para Decimal(), caso contrário o resultado será diferente do resultado esperado quer arredondar para o número inteiro mais próximo (ROUND_HALF_UP) ou para um 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

Note que o método quantize() retorna um número de tipo Decimal, por isso se quiser operar com um número de tipo flutuante, é necessário convertê-lo para um tipo flutuante usando a float(), caso contrário, ocorrerá um erro.

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

Arredondamento de números inteiros para qualquer número de dígitos e arredondamento para números pares

Se quiser arredondar para um dígito inteiro, especificando algo como '10' como primeiro argumento, não lhe dará o resultado desejado.

i = 99518

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

Isto porque quantize() executa arredondamentos de acordo com o expoente do objecto decimal, mas o expoente de Decimal('10') é 0, e não 1.

Pode especificar um expoente arbitrário usando E como fio expoente (por exemplo, '1E1'). O expoente exponente pode ser verificado no 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)

Como está, o resultado será em notação exponencial usando E. Se quiser usar notação normal, ou se quiser operar com int() inteiro após o arredondamento, use int() para converter o 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

Se o arredondamento do argumento for definido para ROUND_HALF_UP, o arredondamento geral ocorrerá, por exemplo, 5 será arredondado para 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

Naturalmente, não há problema se o especificar como um fio.

Definir uma nova função

O método de utilização do módulo decimal é preciso e seguro, mas se não se sentir confortável com a conversão do tipo, pode definir uma nova função para conseguir arredondamentos gerais.

Há muitas maneiras possíveis de o fazer, por exemplo, a seguinte função.

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

Se não precisar de especificar o número de dígitos e arredondar sempre para a primeira casa decimal, pode usar um formulário mais simples.

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

Se precisar de ser preciso, é mais seguro usar decimal.

O seguinte é apenas para referência.

Arredondar as casas decimais para qualquer 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

Ao contrário do arredondamento, 0,5 torna-se 1 por arredondamento geral.

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

Redondo números inteiros a qualquer 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

Ao contrário do arredondamento, 5 passa a ser 10, de acordo com o arredondamento comum.

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

Nota: Para valores negativos

Na função do exemplo acima, -0,5 é arredondado para 0.

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

Há várias formas de pensar sobre arredondamento para valores negativos, mas se quiser fazer -0,5 em -1, pode modificá-lo da seguinte forma, por exemplo

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