Arrotondamento di decimali e interi in Python con “round” e “Decimal.quantize

Attività commerciale

Quanto segue spiega come arrotondare i numeri in Python arrotondando o arrotondando ad un numero pari. Si presume che i numeri siano di tipo float in virgola mobile o int intero.

  • funzione incorporata (per esempio nel linguaggio di programmazione): round()
    • Arrotonda i decimali a qualsiasi numero di cifre.
    • Arrotonda i numeri interi a qualsiasi numero di cifre.
    • round() arrotonda a un numero pari, non a un arrotondamento comune
  • biblioteca standarddecimal quantize()
    • DecimalCreare un oggetto
    • Arrotondamento dei decimali a qualsiasi numero di cifre e arrotondamento ai numeri pari
    • Arrotondamento dei numeri interi a qualsiasi numero di cifre e arrotondamento ai numeri pari
  • Definire una nuova funzione
    • Arrotonda i decimali a qualsiasi numero di cifre.
    • Arrotonda i numeri interi a qualsiasi numero di cifre
    • Nota: per valori negativi

Notate che, come detto sopra, la funzione built-in round non è un arrotondamento generale, ma un arrotondamento ad un numero pari. Vedi sotto per i dettagli.

funzione incorporata (per esempio nel linguaggio di programmazione): round()

Round() è fornita come funzione integrata. Può essere usata senza importare alcun modulo.

Il primo argomento è il numero originale e il secondo argomento è il numero di cifre (a quante cifre arrotondare).

Arrotonda i decimali a qualsiasi numero di cifre.

Quello che segue è un esempio di elaborazione per il tipo float a virgola mobile.

Se il secondo argomento viene omesso, viene arrotondato a un intero. Il tipo diventa anche un tipo int intero.

f = 123.456

print(round(f))
# 123

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

Se il secondo argomento è specificato, restituisce un tipo float in virgola mobile.

Se viene specificato un intero positivo, viene specificata la posizione decimale; se viene specificato un intero negativo, viene specificata la posizione intera. -1 arrotonda al decimo più vicino, -2 arrotonda al centesimo più vicino, e 0 arrotonda a un intero (il primo posto), ma restituisce un tipo float, diversamente da quando viene omesso.

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

Arrotonda i numeri interi a qualsiasi numero di cifre.

Quello che segue è un esempio di elaborazione per il tipo integer int.

Se il secondo argomento viene omesso, o se viene specificato 0 o un intero positivo, il valore originale viene restituito così com'è. Se viene specificato un intero negativo, viene arrotondato alla cifra intera corrispondente. In entrambi i casi, viene restituito un intero di 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() arrotonda a un numero pari, non a un arrotondamento comune

Notate che l'arrotondamento con la funzione built-in round() in Python 3 arrotonda ad un numero pari, non ad un arrotondamento generale.

Come scritto nella documentazione ufficiale, 0,5 è arrotondato a 0, 5 è arrotondato a 0, e così via.

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 definizione di arrotondamento a un numero pari è la seguente.

Se la frazione è minore di 0,5, arrotondala per difetto; se la frazione è maggiore di 0,5, arrotondala per eccesso; se la frazione è esattamente 0,5, arrotondala al numero pari tra l'arrotondamento per difetto e quello per eccesso.
Rounding – Wikipedia

0,5 non è sempre troncato.

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

In alcuni casi, la definizione di arrotondamento a un numero pari non si applica nemmeno all'elaborazione dopo due cifre decimali.

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

Questo è dovuto al fatto che i decimali non possono essere rappresentati esattamente come numeri in virgola mobile, come dichiarato nella documentazione ufficiale.

Il comportamento di round() per i numeri in virgola mobile potrebbe sorprendervi:Per esempio, round(2.675, 2) vi darà 2.67 invece di 2.68 come previsto. Questo non è un bug.:Questo è il risultato del fatto che la maggior parte dei decimali non può essere rappresentata esattamente da numeri in virgola mobile.
round() — Built-in Functions — Python 3.10.2 Documentation

Se volete ottenere un arrotondamento generale o un arrotondamento accurato dei decimali ai numeri pari, potete usare la libreria standard decimal quantize (descritta sotto) o definire una nuova funzione.

Notate anche che round() in Python 2 non è l'arrotondamento ad un numero pari, ma l'arrotondamento.

quantize() della libreria standard decimale

Il modulo decimale della libreria standard può essere usato per gestire numeri decimali esatti in virgola mobile.

Usando il metodo quantize() del modulo decimale, è possibile arrotondare i numeri specificando la modalità di arrotondamento.

I valori impostati per l'argomento di arrotondamento del metodo quantize() hanno i seguenti significati, rispettivamente.

  • ROUND_HALF_UP:Arrotondamento generale
  • ROUND_HALF_EVEN:Arrotondamento ai numeri pari

Il modulo decimale è una libreria standard, quindi non è richiesta alcuna installazione aggiuntiva, ma è necessaria l'importazione.

from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN

Creare un oggetto Decimal

Decimal() può essere usato per creare oggetti di tipo Decimal.

Se si specifica un tipo float come argomento, si può vedere come viene effettivamente trattato il valore.

print(Decimal(0.05))
# 0.05000000000000000277555756156289135105907917022705078125

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

Come mostrato nell'esempio, 0,05 non è trattato esattamente come 0,05. Questo è il motivo per cui la funzione built-in round() descritta sopra arrotonda ad un valore diverso da quello previsto per i valori decimali tra cui 0,05 nell'esempio.

Poiché 0,5 è la metà (-1 potenza di 2), può essere espresso esattamente in notazione binaria.

print(Decimal(0.5))
# 0.5

Se specificate il tipo stringa str invece del tipo float, sarà trattato come il tipo Decimal del valore esatto.

print(Decimal('0.05'))
# 0.05

Arrotondamento dei decimali a qualsiasi numero di cifre e arrotondamento ai numeri pari

Chiama quantize() da un oggetto di tipo Decimal per arrotondare il valore.

Il primo argomento di quantize() è una stringa con lo stesso numero di cifre del numero di cifre che volete trovare, come '0.1' o '0.01'.

Inoltre, l'argomento ROUNDING specifica il modo di arrotondamento; se viene specificato ROUND_HALF_UP, viene usato l'arrotondamento generale.

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 differenza della funzione built-in round(), 0,5 è arrotondato 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

Se l'argomento di arrotondamento è impostato a ROUND_HALF_EVEN, l'arrotondamento viene eseguito ai numeri pari come nella funzione built-in round().

Come menzionato sopra, se un tipo floating-point float è specificato come argomento di Decimal(), è trattato come un oggetto Decimal con un valore uguale al valore effettivo del tipo float, quindi il risultato dell'uso del metodo quantize() sarà diverso da quello atteso, proprio come la funzione built-in 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 l'argomento di Decimal() è specificato come una stringa di tipo str, viene trattato come un oggetto Decimal esattamente di quel valore, quindi il risultato è quello atteso.

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

Poiché 0,5 può essere gestito correttamente dal tipo float, non c'è nessun problema nello specificare il tipo float come argomento di Decimal() quando si arrotonda a un intero, ma è più sicuro specificare il tipo string str quando si arrotonda a un decimale.

Per esempio, 2.675 è in realtà 2.67499…. nel tipo float. Pertanto, se volete arrotondare a due cifre decimali, dovete specificare una stringa a Decimal(), altrimenti il risultato sarà diverso da quello atteso sia che arrotondiate al numero intero più vicino (ROUND_HALF_UP) o ad un numero pari (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

Notate che il metodo quantize() restituisce un numero di tipo Decimal, quindi se volete operare su un numero di tipo float, dovete convertirlo in un tipo float usando float(), altrimenti si verificherà un errore.

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

Arrotondamento dei numeri interi a qualsiasi numero di cifre e arrotondamento ai numeri pari

Se volete arrotondare a una cifra intera, specificare qualcosa come '10' come primo argomento non vi darà il risultato desiderato.

i = 99518

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

Questo perché quantize() esegue l'arrotondamento secondo l'esponente dell'oggetto Decimal, ma l'esponente di Decimal('10') è 0, non 1.

Potete specificare un esponente arbitrario usando E come stringa di esponente (ad esempio, '1E1'). L'esponente esponente può essere controllato nel metodo 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)

Così com'è, il risultato sarà in notazione esponenziale usando E. Se volete usare la notazione normale, o se volete operare con il tipo int intero dopo l'arrotondamento, usate int() per convertire il risultato.

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 l'argomento di arrotondamento è impostato a ROUND_HALF_UP, si verificherà un arrotondamento generale, ad esempio, 5 sarà arrotondato 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

Naturalmente, non c'è nessun problema se lo si specifica come una stringa.

Definire una nuova funzione

Il metodo di usare il modulo decimale è accurato e sicuro, ma se non siete a vostro agio con la conversione dei tipi, potete definire una nuova funzione per ottenere un arrotondamento generale.

Ci sono molti modi possibili per farlo, per esempio la seguente funzione.

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

Se non avete bisogno di specificare il numero di cifre e arrotondate sempre alla prima cifra decimale, potete usare una forma più semplice.

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

Se hai bisogno di essere preciso, è più sicuro usare il decimale.

Quanto segue è solo per riferimento.

Arrotonda i decimali a qualsiasi numero di cifre.

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 differenza del giro, 0,5 diventa 1 come da arrotondamento generale.

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

Arrotonda i numeri interi a qualsiasi numero di cifre

i = 99518

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

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

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

A differenza del giro, 5 diventa 10 come da arrotondamento comune.

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

Nota: per valori negativi

Nella funzione di esempio qui sopra, -0,5 è arrotondato a 0.

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

Ci sono vari modi di pensare all'arrotondamento per i valori negativi, ma se volete rendere -0,5 in -1, potete modificarlo come segue, per esempio

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