15. Aritmetica in virgola mobile: problemi e limitazioni¶

I numeri in virgola mobile sono rappresentati nell’hardware del computer come frazioni base 2 (binarie). Per esempio, la frazione decimale

0.125

ha valore 1/10 + 2/100 + 5/1000 e allo stesso modo la frazione binaria

0.001

ha valore 0/2 + 0/4 + 1/8. Queste due frazioni hanno valori identici,l’unicola vera differenza è che la prima è scritta in notazione frazionaria di base 10 e la seconda in base 2.

Sfortunatamente, la maggior parte delle frazioni decimali non può essere rappresentata esattamente come binaryfractions. Una conseguenza è che, in generale, i numeri decimali a virgola mobile immessi sono solo approssimati dai numeri binari a virgola mobile effettivamente memorizzati nella macchina.

Il problema è più facile da capire all’inizio in base 10. Considera la frazione1 / 3. Puoi approssimarlo come una frazione di base 10:

0.3

o, meglio,

0.33

o, meglio,

0.333

e così via. Non importa quante cifre sei disposto a scrivere, il resultwill non sarà mai esattamente 1/3, ma sarà un’approssimazione sempre migliore di1/3.

Allo stesso modo, indipendentemente dal numero di cifre base 2 che si è disposti a utilizzare, il valore decimale 0.1 non può essere rappresentato esattamente come una frazione base 2. In base2, 1/10 è la frazione infinitamente ripetuta

0.0001100110011001100110011001100110011001100110011...

Si ferma a qualsiasi numero finito di bit e si ottiene un’approssimazione. Sulla maggior parte delle macchine oggi, i float sono approssimati usando una frazione binaria con il numeratore usando i primi 53 bit che iniziano con il bit più significativo e con il denominatore come potenza di due. Nel caso di 1/10, il frazionamento binario è3602879701896397 / 2 ** 55 che è vicino ma non esattamente uguale al vero valore di 1/10.

Molti utenti non sono a conoscenza dell’approssimazione a causa del modo in cui i valori vengono visualizzati. Python stampa solo un’approssimazione decimale al vero decimalvalue dell’approssimazione binaria memorizzata dalla macchina. Sulla maggior parte delle macchine, ifPython doveva stampare il vero valore decimale dell’approssimazione binaria memorizzata per 0.1, sarebbe per visualizzare

>>> 0.10.1000000000000000055511151231257827021181583404541015625

Che è più cifre che la maggior parte delle persone trovano utile, così Python mantiene il numero di cifre gestibile mediante la visualizzazione di un valore arrotondato, invece,

>>> 1 / 100.1

Basta ricordare, anche se il risultato stampato sembra l’esatto valueof 1/10, l’effettivo valore memorizzato è il più vicino rappresentabile frazione binaria.

È interessante notare che ci sono molti numeri decimali diversi che condividono la stessa frazione binaria approssimativa. Ad esempio, i numeri 0.1 e0.10000000000000001 e0.1000000000000000055511151231257827021181583404541015625 sono tutti approssimati da 3602879701896397 / 2 ** 55. Poiché tutti questi decimalvalues condividono la stessa approssimazione, uno di essi potrebbe essere visualizzato pur mantenendo l’invariante eval(repr(x)) == x.

Storicamente, il prompt Python e la funzione incorporatarepr() sceglierebbero quello con 17 cifre significative,0.10000000000000001. A partire dapython 3.1, Python (sulla maggior parte dei sistemi) è ora in grado di scegliere il più breve di questi e semplicemente visualizzare 0.1.

Nota che questo è nella natura stessa del binario a virgola mobile: questo non è un bugin Python, e non è nemmeno un bug nel tuo codice. Vedrai lo stesso tipo di qualcosa in tutte le lingue che supportano l’aritmetica in virgola mobile dell’hardware(anche se alcune lingue potrebbero non visualizzare la differenza per impostazione predefinita o in modalità alloutput).

Per un output più piacevole, è possibile utilizzare la formattazione delle stringhe per produrre un numero limitato di cifre significative:

>>> format(math.pi, '.12g') # give 12 significant digits'3.14159265359'>>> format(math.pi, '.2f') # give 2 digits after the point'3.14'>>> repr(math.pi)'3.141592653589793'

È importante rendersi conto che questa è, in senso reale, un’illusione: si sta semplicemente arrotondando la visualizzazione del vero valore della macchina.

Un’illusione può generarne un’altra. Per esempio, poiché 0.1 non è esattamente 1/10,sommando i tre valori di 0,1 non può produrre esattamente 0.3, sia:

>>> .1 + .1 + .1 == .3False

Inoltre, poiché l’0.1 non può ottenere qualsiasi più vicino al valore esatto di 1/10 e0.3 non è possibile ottenere più vicino al valore esatto di 3/10, quindi pre-arrotondamento conround() funzione non può aiutare:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)False

anche Se i numeri non possono essere avvicinate a loro destinati i valori esatti,il round() funzione può essere utile per la post-arrotondamento in modo che resultswith inesatte valori diventano paragonabili l’uno all’altro:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)True

Binario aritmetica a virgola mobile di riserva molte sorprese come questa. Il problema con ” 0.1 “è spiegato in dettaglio preciso di seguito, nella sezione”Errore di rappresentazione”. Vedere I pericoli di Floating Pointfor un resoconto più completo di altre sorprese comuni.

Come dice verso la fine, ” non ci sono risposte facili.”Ancora, non essere undulywary di virgola mobile! Gli errori nelle operazioni float Python sono ereditati dall’hardware in virgola mobile e sulla maggior parte delle macchine sono nell’ordine di nomore di 1 parte in 2**53 per operazione. Questo è più che adeguato per la maggior parte dei compiti, ma è necessario tenere presente che non si tratta di aritmetica decimale e che ogni operazione float può subire un nuovo errore di arrotondamento.

Mentre esistono casi patologici, per l’uso più casuale di floating-pointarithmetic vedrai il risultato che ti aspetti alla fine se arrotonderai semplicemente il display dei risultati finali al numero di cifre decimali che ti aspetti.str() di solito è sufficiente, e per un controllo più fine vedere gli specificatori di formato del metodo str.format()nella sintassi della stringa di formato.

Per i casi d’uso che richiedono una rappresentazione decimale esatta, provare a utilizzare il modulodecimal che implementa l’aritmetica decimale adatta per applicazioni di contabilizzazione e applicazioni ad alta precisione.

Un’altra forma di aritmetica esatta è supportata dal modulofractions che implementa l’aritmetica basata su numeri razionali (quindi i numeri come1 / 3 possono essere rappresentati esattamente).

Se sei un utente pesante di operazioni in virgola mobile dovresti dare un’occhiata al pacchetto Python numerico e a molti altri pacchetti per operazioni matematiche e statistiche forniti dal progetto SciPy. Vedere <https://scipy.org>.

Python fornisce strumenti che possono aiutare in quelle rare occasioni in cui vuoi davvero conoscere il valore esatto di un float. Il metodofloat.as_integer_ratio() esprime il valore di un float come afraction:

>>> x = 3.14159>>> x.as_integer_ratio()(3537115888337719, 1125899906842624)

Dal momento che il rapporto è esatta, può essere utilizzato senza perdita di qualità per ricreare theoriginal valore:

>>> x == 3537115888337719 / 1125899906842624True

float.hex() metodo esprime un galleggiante in esadecimale (base16), di nuovo dare l’esatto valore memorizzato dal computer:

>>> x.hex()'0x1.921f9f01b866ep+1'

Questo preciso rappresentazione esadecimale può essere utilizzato per reconstructthe valore float esattamente:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')True

Poiché la rappresentazione è esatta, è utile per il porting affidabile di valuesacross diverse versioni di Python (indipendenza dalla piattaforma) e lo scambiodati con altri linguaggi che supportano lo stesso formato (come Java e C99).

Un altro strumento utile è la funzionemath.fsum() che aiuta a mitigare la perdita di precisione durante la sommatoria. Tiene traccia delle “cifre perse” come valori aggiunti su un totale corrente. Ciò può fare la differenza nell’accuratezza complessivain modo che gli errori non si accumulino fino al punto in cui influenzano il totale finale:

>>> sum( * 10) == 1.0False>>> math.fsum( * 10) == 1.0True

15.1. Errore di rappresentazione¶

Questa sezione spiega l’esempio “0.1” in dettaglio e mostra come è possibile eseguire un’analisi esatta di casi come questo. Si assume una familiarità di base con la rappresentazione binaryfloating-point.

L’errore di rappresentazione si riferisce al fatto che alcune (la maggior parte, in realtà)frazioni decimali non possono essere rappresentate esattamente come frazioni binarie (base 2).Questo è il motivo principale per cui Python (o Perl, C, C++, Java, Fortran e molti altri) spesso non visualizzerà il numero decimale esatto che ci si aspetta.

Perché? 1/10 non è esattamente rappresentabile come frazione binaria. Quasi allmachines oggi (novembre 2000) usa l’aritmetica in virgola mobile IEEE-754, e quasi tutte le piattaforme mappano Python float a IEEE-754 “double precision”. 754doubles contengono 53 bit di precisione, quindi su input il computer si sforza di convertire 0.1 alla frazione più vicina che può della forma J / 2 * * N dove J èun intero contenente esattamente 53 bit. La riscrittura

1 / 10 ~= J / (2**N)
J ~= 2**N / 10

e ricordando che J ha esattamente 53 bit (è >= 2**52 ma < 2**53),il miglior valore per N 56:

>>> 2**52 <= 2**56 // 10 < 2**53True

Che è, 56 è il solo valore di N che lascia J con esattamente 53 bit. Il miglior valore possibile per J è quindi quel quoziente arrotondato:

>>> q, r = divmod(2**56, 10)>>> r6

Poiché il resto è oltre la metà di 10, la migliore approssimazione è obtainedby arrotondamento:

>>> q+17205759403792794

Pertanto, la migliore approssimazione di 1/10 nel 754 precisione doppia è:

7205759403792794 / 2 ** 56

Dividere sia il numeratore e il denominatore da due riduce la frazione a:

3602879701896397 / 2 ** 55

si noti che, poiché abbiamo arrotondato, questo è in realtà un po ‘ più grandi 1/10;se non avessimo arrotondato, il quoziente sarebbe stato un po ‘ più piccolo di1/10. Ma in nessun caso può essere esattamente 1/10!

il computer non “vede” 1/10: quello che si vede è l’esatto frazione givenabove, i migliori 754 doppio approssimazione si può ottenere:

>>> 0.1 * 2 ** 553602879701896397.0

Se moltiplichiamo questa frazione di 10**55, possiamo vedere il valore to55 cifre decimali:

>>> 3602879701896397 * 10 ** 55 // 2 ** 551000000000000000055511151231257827021181583404541015625

il che significa che il numero esatto memorizzati nel computer è pari al valore decimale 0.1000000000000000055511151231257827021181583404541015625.Invece di visualizzare l’intero valore decimale, molte lingue (includingolder versioni di Python), si arrotonda il risultato al 17 cifre significative:

>>> format(0.1, '.17f')'0.10000000000000001'

fractions e decimal moduli di fare questi calculationseasy:

>>> from decimal import Decimal>>> from fractions import Fraction>>> Fraction.from_float(0.1)Fraction(3602879701896397, 36028797018963968)>>> (0.1).as_integer_ratio()(3602879701896397, 36028797018963968)>>> Decimal.from_float(0.1)Decimal('0.1000000000000000055511151231257827021181583404541015625')>>> format(Decimal.from_float(0.1), '.17')'0.10000000000000001'

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *