decimal - matematica con virgola fissa e mobile

Scopo Artimetica decimale usando numeri a virgola fissa e mobile
Versione Python 2.4 e superiore

Il modulo decimal implementa l'aritmetica a virgola fissa e mobile usando il modello familiare alla maggior parte delle persone, piuttosto che la versione a virgola mobile IEEE implementata dalla maggior parte dell'hardware dei computer. Una istanza di Decimal può rappresentare esattamente un qualsiasi numero, arrotondare 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 chiede un intero come parametro, oppure una stringa. I numeri a virgola mobile devono essere convertiti a stringa prima di essere usati per creare un Decimal, lasciando che sia il chiamante a gestire esplicitamente il numero di cifre per i valori che non possono essere espressi esattamente usando rappresentazioni hardware di virgola mobile.

import decimal

fmt = '{0:<20} {1:<20}'
print fmt.format('Input', 'Output')
print fmt.format('-' * 20, '-' * 20)

# Intero
print fmt.format(5, decimal.Decimal(5))

# Stringa
print fmt.format('3.14', decimal.Decimal('3.14'))

# Float
print fmt.format(repr(0.1), decimal.Decimal(str(0.1)))

Si nota che il valore a virgola mobile di 0.1 non è rappresentato come un valore esatto, quindi la rappresentazione come float è diversa dal valore Decimal.

$ python decimal_create.py
Input                Output
-------------------- --------------------
5                    5
3.14                 3.14
0.10000000000000001  0.1

Meno convenientemente, i decimali possono anche essere creati da tuple che contengono un flag di segno (0 per positivo, 1 per negativo), una tupla di cifre, ed un esponente intero.

import decimal

# Tuple
t = (1, (1, 1), -2)
print 'Input   :', t
print 'Decimale:', decimal.Decimal(t)
$ python decimal_tuple.py
Input   : (1, (1, 1), -2)
Decimale: -0.11

Aritmetica

Decimal sovrascrive gli operatori aritmetici semplici, così che una volta che si ha un valore lo si può manipolare pressochè allo stesso modo dei tipi numerici built-in.

import decimal

a = decimal.Decimal('5.1')
b = decimal.Decimal('3.14')
c = 4
d = 3.14

print 'a     =', a
print 'b     =', b
print 'c     =', c
print 'd     =', 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 =',
try:
    print a + d
except TypeError, e:
    print e

Gli operatori decimali accettano anche parametri interi, ma i valori a virgola mobile devono essere convertiti in istanze di Decimal.

$ python decimal_operators.py
a     = 5.1
b     = 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' and 'float'

Logaritmi

Oltre l'aritmetica base, Decimal include dei metodi per trovare i logaritmi di base 10 e naturali.

import decimal

d = decimal.Decimal(100)
print 'd     :', d
print 'log10 :', d.log10()
print 'ln    :', d.ln()
$ python decimal_log.py
d     : 100
log10 : 2
ln    : 4.605170185988091368035982909

Valori speciali

Oltre ai valori numerici che ci si aspetta, Decimal può rappresentare parecchi valori speciali, inclusi valori positivi e negativi per l'infinito, "non un numero" (NaN), e zero.

import decimal

for value in [ 'Infinity', 'NaN', '0' ]:
    print decimal.Decimal(value), decimal.Decimal('-' + value)
print

# Matematica con infinity
print 'Infinito + 1:', (decimal.Decimal('Infinity') + 1)
print '-Infinito + 1:', (decimal.Decimal('-Infinity') + 1)

# Stampa le comparazioni di NaN
print decimal.Decimal('NaN') == decimal.Decimal('Infinity')
print decimal.Decimal('NaN') != decimal.Decimal(1)

L'aggiungere valori ad infinito restituisce un altro valore infinito. Il confronto per eguaglianza con NaN (non numeri - n.d.t.) restituisce sempre False ed il confronto per diseguaglianza restituisce sempre True. Il confronto per ordinamento contro NaN è indefinito e restituisce sempre un errore.

$ python decimal_special.py Infinity -Infinity
NaN -NaN
0 -0

Infinito + 1: Infinity
-Infinito + 1: -Infinity
False
True

Contesto

Fino ad qui tutti gli esempi hanno usato i comportamenti predefiniti del modulo decimal. E' possibile sovrascrivere le impostazioni tipo il mantenimento della precisione, come viene eseguito l'arrotondamento, la gestione di errori, ecc. Tutte queste impostazioni sono gestite tramite un context. Context può essere applicato a tutte le istanze di Decimal in un thread o localmente all'interno di una piccola regione di codice.

Contesto corrente

Per ottenere il contesto globale corrente, si usa getcontext().

import decimal

print decimal.getcontext()
$ python decimal_getcontext.py
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999999, Emax=999999999, capitals=1, flags=[], traps=[Overflow, InvalidOperation, DivisionByZero])

Precisione

L'attributo prec di context controlla la precisione mantenuta per i nuovi valori creati come risultato aritmetico. I valori letterali sono mantenuti come descritto.

import decimal

d = decimal.Decimal('0.123456')

for i in range(4):
    decimal.getcontext().prec = i
    print i, ':', d, d * 1
$ python decimal_precision.py
0 : 0.123456 0
1 : 0.123456 0.1
2 : 0.123456 0.12
3 : 0.123456 0.123

Arrotondamento

Ci sono parecchie opzioni di arrotondamento per manterere i valori con la precisione desiderata.

ROUND_CEILING
Arrotonda sempre verso l'infinito
ROUND_DOWN
Arrotonda sempre verso lo zero
ROUND_FLOOR
Arrotonda sempre verso l'infinito negativo
ROUND_HALF_DOWN
Arronda per eccesso se l'ultima cifra significativa è maggiore/uguale a 5, altrimenti per difetto
ROUND_HALF_EVEN
Come ROUND_HALF_DOWN eccetto che se il valore è 5, allora viene esaminata la cifra che precede. Valori pari fanno arrotondare in difetto e valori pari fanno arrotondare 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.
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 = '{0:20} {1:^10} {2:^10} {3:^10}'

print 'POSITIVI:'
print

print header_fmt.format(' ', '1/8 (1)', '1/8 (2)', '1/8 (3)')
print header_fmt.format(' ', '-' * 10, '-' * 10, '-' * 10)
for rounding_mode in ROUNDING_MODES:
    print '{0:20}'.format(rounding_mode),
    for precision in [ 1, 2, 3 ]:
        context.prec = precision
        context.rounding = getattr(decimal, rounding_mode)
        value = decimal.Decimal(1) / decimal.Decimal(8)
        print '{0:<10}'.format(value),
    print

print
print 'NEGATIVI:'

print header_fmt.format(' ', '-1/8 (1)', '-1/8 (2)', '-1/8 (3)')
print header_fmt.format(' ', '-' * 10, '-' * 10, '-' * 10)
for rounding_mode in ROUNDING_MODES:
    print '{0:20}'.format(rounding_mode),
    for precision in [ 1, 2, 3 ]:
        context.prec = precision
        context.rounding = getattr(decimal, rounding_mode)
        value = decimal.Decimal(-1) / decimal.Decimal(8)
        print '{0:<10}'.format(value),
    print
$ python decimal_rounding.py
POSITIVI:

                      1/8 (1)    1/8 (2)    1/8 (3)
                     ---------- ---------- ----------
ROUND_CEILING        0.2        0.13       0.125
ROUND_DOWN           0.1        0.12       0.125
ROUND_FLOOR          0.1        0.12       0.125
ROUND_HALF_DOWN      0.1        0.12       0.125
ROUND_HALF_EVEN      0.1        0.12       0.125
ROUND_HALF_UP        0.1        0.13       0.125
ROUND_UP             0.2        0.13       0.125
ROUND_05UP           0.1        0.12       0.125

NEGATIVI:
                      -1/8 (1)   -1/8 (2)   -1/8 (3)
                     ---------- ---------- ----------
ROUND_CEILING        -0.1       -0.12      -0.125
ROUND_DOWN           -0.1       -0.12      -0.125
ROUND_FLOOR          -0.2       -0.13      -0.125
ROUND_HALF_DOWN      -0.1       -0.12      -0.125
ROUND_HALF_EVEN      -0.1       -0.12      -0.125
ROUND_HALF_UP        -0.1       -0.13      -0.125
ROUND_UP             -0.2       -0.13      -0.125
ROUND_05UP           -0.1       -0.12      -0.125

Contesto locale

Con Python 2.5 o superiore si può anche applicare context ad un sottoinsieme del proprio codice con una istruzione with ed un gestore di context.

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)
$ python decimal_context_manager.py
Precisione locale: 2
3.14 / 3 = 1.0

Precisione predefinita: 28
3.14 / 3 = 1.046666666666666666666666667

Context per istanza

Context può essere usato per costruire istanze di Decimal, applicando i parametri di precisione ed arrotondamento alla conversione dal tipo in input. Questo consente alla propria applicazione di selezionare la precisione dei valori costanti separatamente dalla precisione per i dati dell'utente.

import decimal

# Imposta un contesto con precisione limitata
c = decimal.getcontext().copy()
c.prec = 3

# Crea la costante
pi = c.create_decimal('3.1415')

# Il valore costante viene arrotondato
print 'PI:', pi

print 'RESULT:', decimal.Decimal('2.01') * pi
$ python decimal_instance_context.py
PI: 3.14
RESULT: 6.3114

Thread

Il context "globale" è in realtà locale al thread, quindi ogni thread può essere potenzialmente configurato usando valori diversi.

import decimal
import threading
from Queue import Queue

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) )
        return

a = decimal.Decimal('3.14')
b = decimal.Decimal('1.234')
q = Queue()
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 prec, '\t', value
$ python 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.
Wikipedia: Numero in virgola mobile
Un articolo sulle rappresentazioni e l'aritmetica a virgola mobile