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.