decimal - Matematica per Valori Fissi ed a Virgola Mobile
Scopo: Aritmetica decimale usando valori fissi ed a virgola mobile
Il modulo decimal implementa aritmetica su valori fissi ed a virgola mobile usando il modello familiare alla maggior parte delle persone, piuttosto che la versione a virgola mobile IEEE implementata nella maggior parte dell'hardware dei computer e familiare ai programmatori. Una istanza di Decimal
può rappresentare esattamente qualsiasi numero, arrotondarlo per eccesso o difetto, ed applicare un limite al numero delle cifre significative.
Decimal
I valori decimali sono rappresentati come istanze della classe Decimal
. Il costruttore riceve come argomento un intero od una stringa. Valori a virgola mobile possono essere convertiti in stringa prima di essere usati per creare un Decimal
, consentendo al chiamante di gestire esplicitamente il numero di cifre per valori che non possono essere espressi esattamente usando le rappresentazioni hardware a virgola mobile. Alternativamente, il metodo di classe from_float()
converte all'esatta rappresentazione decimale.
# decimal_create.py
import decimal
fmt = '{0:<25} {1:<25}'
print(fmt.format('Input', 'Output'))
print(fmt.format('-' * 25, '-' * 25))
# Intero
print(fmt.format(5, decimal.Decimal(5)))
# Stringa
print(fmt.format('3.14', decimal.Decimal('3.14')))
# Virgola mobile
f = 0.1
print(fmt.format(repr(f), decimal.Decimal(str(f))))
print('{:<0.23g} {:<25}'.format(
f,
str(decimal.Decimal.from_float(f))[:25])
)
Il valore a virgola mobile di 0.1
non è rappresentato come valore esatto in binario, quindi la rappresentazione come float
è diversa dal valore Decimal
. L'intera rappresentazione stringa è troncata a 25 caratteri nell'ultima riga del risultato.
$ python3 decimal_create.py Input Output ------------------------- ------------------------- 5 5 3.14 3.14 0.1 0.1 0.10000000000000000555112 0.10000000000000000555111
I Decimal
possono anche essere creati da tuple che contengano un segno (0
positivo, 1
negativo), una tupla di cifre ed un esponente intero.
# decimal_tuple.py
import decimal
# Tuple
t = (1, (1, 1), -2)
print('Input :', t)
print('Decimal:', decimal.Decimal(t))
La rappresentazione basata sulla tupla è meno conveniente da creare, ma offre un modo portabile per esportare valori decimali senza perdere precisione. La forma in tupla può essere trasmessa attraverso una rete oppure essere conservata in un database che non supporti valori decimali accurati, quindi riconvertita in una istanza di Decimal
successivamente.
$ python3 decimal_tuple.py Input : (1, (1, 1), -2) Decimal: -0.11
Formattazione
I Decimal
si conformano al protocollo di formattazione di stringhe usando le stesse sintassi ed opzioni degli altri tipi numerici.
# decimal_format.py
import decimal
d = decimal.Decimal(1.1)
print('Precisione:')
print('{:.1}'.format(d))
print('{:.2}'.format(d))
print('{:.3}'.format(d))
print('{:.18}'.format(d))
print('\nLarghezza e precisione combinate:')
print('{:5.1f} {:5.1g}'.format(d, d))
print('{:5.2f} {:5.2g}'.format(d, d))
print('{:5.2f} {:5.2g}'.format(d, d))
print('\nRiempimento con Zero:')
print('{:05.1}'.format(d))
print('{:05.2}'.format(d))
print('{:05.3}'.format(d))
Le stringhe di formato possono controllare la larghezza dell'output, la precisione (il numero di cifre significative), e come allineare il valore per riempire la larghezza.
$ python3 decimal_format.py Precisione: 1 1.1 1.10 1.10000000000000009 Larghezza e precisione combinate: 1.1 1 1.10 1.1 1.10 1.1 Riempimento con Zeri: 00001 001.1 01.10
Aritmetica
Decimal
fa un overload degli operatori matematici semplici, quindi le istanze possono essere manipolate in modo pressochè uguale ai tipi numerici built-in.
# decimal_operators.py
import decimal
a = decimal.Decimal('5.1')
b = decimal.Decimal('3.14')
c = 4
d = 3.14
print('a =', repr(a))
print('b =', repr(b))
print('c =', repr(c))
print('d =', repr(d))
print()
print('a + b =', a + b)
print('a - b =', a - b)
print('a * b =', a * b)
print('a / b =', a / b)
print()
print('a + c =', a + c)
print('a - c =', a - c)
print('a * c =', a * c)
print('a / c =', a / c)
print()
print('a + d =', end=' ')
try:
print(a + d)
except TypeError as e:
print(e)
Gli operatori di Decimal
accettano argomenti interi, ma i valori a virgola mobile devono essere convertiti in istanze Decimal
.
$ python3 decimal_operators.py a = Decimal('5.1') b = Decimal('3.14') c = 4 d = 3.14 a + b = 8.24 a - b = 1.96 a * b = 16.014 a / b = 1.624203821656050955414012739 a + c = 9.1 a - c = 1.1 a * c = 20.4 a / c = 1.275 a + d = unsupported operand type(s) for +: 'decimal.Decimal' and 'float'
Altre all'aritmetica basica, Decimal
include metodi per trovare i logaritmi naturali ed a base 10. I valori di ritorno di log10()
e ln()
sono istanze Decimal
.
Valori Speciali
Oltre agli attesi valori numerici, Decimal
può rappresentare parecchi valori speciali, inclusi valori positivi e negativo per Infinity (infinito) e NaN (non un numero) e zero.
# decimal_special.py
import decimal
for value in ['Infinity', 'NaN', '0']:
print(decimal.Decimal(value), decimal.Decimal('-' + value))
print()
# Math with infinity
print('Infinity + 1:', (decimal.Decimal('Infinity') + 1))
print('-Infinity + 1:', (decimal.Decimal('-Infinity') + 1))
# Print comparing NaN
print(decimal.Decimal('NaN') == decimal.Decimal('Infinity'))
print(decimal.Decimal('NaN') != decimal.Decimal(1))
Aggiungendo valori ad infinito si ottiene sempre un altro valore infinito. Un confronto di uguaglianza con NaN
ritorna sempre False
ed un confronto per ineguaglianza ritorna sempre True
. Confrontare per un ordinamento contro NaN
è indefinito e provoca un errore.
$ python3 decimal_special.py Infinity -Infinity NaN -NaN 0 -0 Infinity + 1: Infinity -Infinity + 1: -Infinity False True
Contesto
Fin qui tutti gli esempi hanno usato i comportamenti predefiniti del modulo decimal. E' possibile sostituire questi valori come la precisione mantenuta, come viene eseguito l'arrotondamento, la gestione degli errori ecc., usando un contesto. I contesti possono essere applicati alle istanze di Decimal
in un thread o localmente all'interno di una piccola porzione di codice.
Contesto Corrente
Per ottenere il contesto corrente globale, si usa getcontext()
.
# decimal_getcontext.py
import decimal
context = decimal.getcontext()
print('Emax =', context.Emax)
print('Emin =', context.Emin)
print('capitals =', context.capitals)
print('prec =', context.prec)
print('rounding =', context.rounding)
print('flags =')
for f, v in context.flags.items():
print(' {}: {}'.format(f, v))
print('traps =')
for t, v in context.traps.items():
print(' {}: {}'.format(t, v))
Lo script di esempio mostra le proprietà pubbliche di un Context
.
$ python3 decimal_getcontext.py Emax = 999999 Emin = -999999 capitals = 1 prec = 28 rounding = ROUND_HALF_EVEN flags = <class 'decimal.InvalidOperation'>: False <class 'decimal.FloatOperation'>: False <class 'decimal.DivisionByZero'>: False <class 'decimal.Overflow'>: False <class 'decimal.Underflow'>: False <class 'decimal.Subnormal'>: False <class 'decimal.Inexact'>: False <class 'decimal.Rounded'>: False <class 'decimal.Clamped'>: False traps = <class 'decimal.InvalidOperation'>: True <class 'decimal.FloatOperation'>: False <class 'decimal.DivisionByZero'>: True <class 'decimal.Overflow'>: True <class 'decimal.Underflow'>: False <class 'decimal.Subnormal'>: False <class 'decimal.Inexact'>: False <class 'decimal.Rounded'>: False <class 'decimal.Clamped'>: False
Precisione
L'attributo prec
del contesto controlla la precisione mantenuta per i nuovi valori creati come risultato di operazioni aritmetiche. I valori letterali sono mantenuti come descritti.
# decimal_precision.py
import decimal
d = decimal.Decimal('0.123456')
for i in range(1, 5):
decimal.getcontext().prec = i
print(i, ':', d, d * 1)
Per modificare la precisione, si assegna un nuovo valore tra 1
e decimal.MAX_PREC
direttamente all'attributo.
$ python3 decimal_precision.py 1 : 0.123456 0.1 2 : 0.123456 0.12 3 : 0.123456 0.123 4 : 0.123456 0.1235
Arrotondamento
Ci sono parecchie opzioni di arrotondamento per mantenere i valori alla precisione desiderata.
- ROUND_CEILING
- Arrotonda sempre per eccesso verso infinito.
- ROUND_DOWN
- Arrotonda sempre per difetto.
- ROUND_FLOOR
- Arrotonda sempre per difetto verso infinito negativo.
- ROUND_HALF_DOWN
- Arrotonda per eccesso se l'ultima cifra significativa è maggiore od uguale a 5, altrimenti per difetto.
- ROUND_HALF_EVEN
- Come ROUND_HALF_DOWN eccetto che se il valore è 5, viene esaminata la cifra precedente. Valori pari causano un arrotondamento per difetto e valori dispari per eccesso.
- ROUND_HALF_UP
- Come ROUND_HALF_DOWN eccetto che se l'ultima cifra significativa è 5, il valore viene arrotondato per eccesso.
- ROUND_UP
- Arrotonda per eccesso.
- ROUND_05UP
- Arrotonda per eccesso se l'ultima cifra è 0 o 5, altrimenti per difetto.
# decimal_rounding.py
import decimal
context = decimal.getcontext()
ROUNDING_MODES = [
'ROUND_CEILING',
'ROUND_DOWN',
'ROUND_FLOOR',
'ROUND_HALF_DOWN',
'ROUND_HALF_EVEN',
'ROUND_HALF_UP',
'ROUND_UP',
'ROUND_05UP',
]
header_fmt = '{:10} ' + ' '.join(['{:^8}'] * 6)
print(header_fmt.format(
' ',
'1/8 (1)', '-1/8 (1)',
'1/8 (2)', '-1/8 (2)',
'1/8 (3)', '-1/8 (3)',
))
for rounding_mode in ROUNDING_MODES:
print('{0:10}'.format(rounding_mode.partition('_')[-1]),
end=' ')
for precision in [1, 2, 3]:
context.prec = precision
context.rounding = getattr(decimal, rounding_mode)
value = decimal.Decimal(1) / decimal.Decimal(8)
print('{0:^8}'.format(value), end=' ')
value = decimal.Decimal(-1) / decimal.Decimal(8)
print('{0:^8}'.format(value), end=' ')
print()
Questo programma mostra gli effetti dell'arrotondamento dello stesso valore con diversi livelli di precisione usando diversi algoritmi.
$ python3 decimal_rounding.py 1/8 (1) -1/8 (1) 1/8 (2) -1/8 (2) 1/8 (3) -1/8 (3) CEILING 0.2 -0.1 0.13 -0.12 0.125 -0.125 DOWN 0.1 -0.1 0.12 -0.12 0.125 -0.125 FLOOR 0.1 -0.2 0.12 -0.13 0.125 -0.125 HALF_DOWN 0.1 -0.1 0.12 -0.12 0.125 -0.125 HALF_EVEN 0.1 -0.1 0.12 -0.12 0.125 -0.125 HALF_UP 0.1 -0.1 0.13 -0.13 0.125 -0.125 UP 0.2 -0.2 0.13 -0.13 0.125 -0.125 05UP 0.1 -0.1 0.12 -0.12 0.125 -0.125
Contesto Locale
Il contesto si può applicare ad un blocco di codice con l'istruzione with
.
# decimal_context_manager.py
import decimal
with decimal.localcontext() as c:
c.prec = 2
print('Precisione locale:', c.prec)
print('3.14 / 3 =', (decimal.Decimal('3.14') / 3))
print()
print('Precisione predefinita:', decimal.getcontext().prec)
print('3.14 / 3 =', (decimal.Decimal('3.14') / 3))
Context
supporta l'API del gestore di contesto usata da with
quindi le impostazioni vengono applicate solo all'interno del blocco.
$ python3 decimal_context_manager.py Precisione locale: 2 3.14 / 3 = 1.0 Precisione predefinita: 28 3.14 / 3 = 1.046666666666666666666666667
Contesto Per Istanza
Un Context
può anche essere usato per costruire istanze di Decimal
, che ereditano gli argomenti di precisione ed arrotondamento dalla conversione dal contesto.
# decimal_instance_context.py
import decimal
# Imposta un contesto con precisione limiata
c = decimal.getcontext().copy()
c.prec = 3
# Crea una propria costante
pi = c.create_decimal('3.1415')
# Il falore della costante viene arrotondato
print('PI GRECO :', pi)
# Il risultato derivato dall'uso della costante usa il contesto globale
print('RISULTATO:', decimal.Decimal('2.01') * pi)
Questo consente ad una applicazione di selezionare la precisione di valori costanti a parte rispetto alla precisione dei dati utente, ad esempio.
$ python3 decimal_instance_context.py PI GRECO : 3.14 RISULTATO: 6.3114
Thread
Il contesto "globale" in realtà è locale rispetto al thread, quindi ogni thread può essere potenzialmente configurato usando valori diversi.
# decimal_thread_context.py
import decimal
import threading
from queue import PriorityQueue
class Multiplier(threading.Thread):
def __init__(self, a, b, prec, q):
self.a = a
self.b = b
self.prec = prec
self.q = q
threading.Thread.__init__(self)
def run(self):
c = decimal.getcontext().copy()
c.prec = self.prec
decimal.setcontext(c)
self.q.put((self.prec, a * b))
a = decimal.Decimal('3.14')
b = decimal.Decimal('1.234')
# Una PriorityQueue ritornerà valori ordinati per precisione,
# a prescindere dall'ordine nel quale i thread finiscono.
q = PriorityQueue()
threads = [Multiplier(a, b, i, q) for i in range(1, 6)]
for t in threads:
t.start()
for t in threads:
t.join()
for i in range(5):
prec, value = q.get()
print('{} {}'.format(prec, value))
Questo esempio crea un nuovo contesto usando quello specificato, e lo installa all'interno di ogni thread.
$ python3 decimal_thread_context.py 1 4 2 3.9 3 3.87 4 3.875 5 3.8748
Vedere anche:
- decimal
- La documentazione della libreria standard per questo modulo
- Note di portabilità
- Note di portabilità per decimal
- Wikipedia: Numero in virgola mobile
- Articolo sulla rappresentazione ed aritmetica dei numeri a virgola mobile.
- Floating Point Arithmetic: Issues and Limitations
- Articolo dai tutorial Python che descrive i problemi relativi alla aritmetica dei numeri a virgola mobile.