Perché il valore in virgola mobile di 4 * 0.1 sembra carino in Python 3 ma 3 * 0.1 no?


158

So che la maggior parte dei decimali non ha un'esatta rappresentazione in virgola mobile (la matematica in virgola mobile è rotta? ).

Ma non vedo perché 4*0.1sia stampato bene come 0.4, ma 3*0.1non lo è, quando entrambi i valori hanno effettivamente brutte rappresentazioni decimali:

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')

7
Perché alcuni numeri possono essere rappresentati esattamente, altri no.
Morgan Thrapp,

58
@MorganThrapp: no non lo è. L'OP chiede quale sia la scelta di formattazione dall'aspetto piuttosto arbitrario. Né 0,3 né 0,4 possono essere rappresentati esattamente in virgola mobile binaria.
Bathsheba,

42
@BartoszKP: Dopo aver letto più volte di documenti, non spiega il motivo per cui Python è la visualizzazione di 0.3000000000000000444089209850062616169452667236328125come 0.30000000000000004e 0.40000000000000002220446049250313080847263336181640625quanto .4, anche se sembrano avere la stessa precisione, e quindi non rispondere alla domanda.
Mooing Duck,

6
Vedi anche stackoverflow.com/questions/28935257/… - Sono un po 'irritato dal fatto che sia stato chiuso come duplicato ma questo no.
Casuale 832

12
Riaperto, per favore non chiuderlo come duplicato di "la matematica in virgola mobile è rotta" .
Antti Haapala,

Risposte:


301

La risposta semplice è perché a 3*0.1 != 0.3causa dell'errore di quantizzazione (arrotondamento) (mentre 4*0.1 == 0.4perché moltiplicare per una potenza di due è di solito un'operazione "esatta").

Puoi usare il .hexmetodo in Python per visualizzare la rappresentazione interna di un numero (sostanzialmente, il valore binario esatto in virgola mobile, piuttosto che l'approssimazione base-10). Questo può aiutare a spiegare cosa sta succedendo sotto il cofano.

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0.1 è 0x1.999999999999a volte 2 ^ -4. La "a" alla fine indica la cifra 10 - in altre parole, 0,1 in virgola mobile binaria è leggermente più grande del valore "esatto" di 0,1 (perché l'ultimo 0x0,99 viene arrotondato per eccesso a 0x0.a). Quando si moltiplica questo per 4, una potenza di due, l'esponente si sposta verso l'alto (da 2 ^ -4 a 2 ^ -2) ma il numero è altrimenti invariato, quindi 4*0.1 == 0.4.

Tuttavia, quando si moltiplica per 3, la piccola piccola differenza tra 0x0,99 e 0x0.a0 (0x0,07) si ingrandisce in un errore 0x0,15, che si presenta come un errore di una cifra nell'ultima posizione. Questo fa sì che 0,1 * 3 sia leggermente più grande del valore arrotondato di 0,3.

Il float di Python 3 reprè progettato per essere round-trippable , ovvero il valore mostrato dovrebbe essere esattamente convertibile nel valore originale. Pertanto, non è possibile visualizzare 0.3e 0.1*3esattamente nello stesso modo, o le due differenti numeri finirebbe stesso dopo round-tripping. Di conseguenza, il reprmotore di Python 3 sceglie di visualizzarne uno con un leggero errore apparente.


25
Questa è una risposta incredibilmente completa, grazie. (In particolare, grazie per .hex()averlo mostrato ; non sapevo che esistesse.)
NPE,

21
@supercat: Python cerca di trovare la stringa più corta che si arrotonda al valore desiderato , qualunque cosa accada. Ovviamente il valore valutato deve essere compreso entro 0,5 ppm (o si arrotonderebbe a qualcos'altro), ma potrebbe richiedere più cifre in casi ambigui. Il codice è molto gnarly, ma se vuoi dare un'occhiata: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo

2
@supercat: sempre la stringa più corta entro 0,5 ulp. ( Rigorosamente all'interno se stiamo guardando un float con LSB dispari; cioè, la stringa più corta che lo fa funzionare con legami tondi-pari-pari). Eventuali eccezioni a questo sono un bug e devono essere segnalate.
Mark Dickinson,

7
@MarkRansom Sicuramente hanno usato qualcos'altro se non eche è già una cifra esadecimale. Forse pper potere invece di esponente .
Bergi,

11
@Bergi: l'uso di pin questo contesto risale (almeno) a C99 e appare anche in IEEE 754 e in varie altre lingue (incluso Java). Quando float.hexe float.fromhexsono stati implementati (da me :-), Python stava semplicemente copiando ciò che era ormai una pratica consolidata. Non so se l'intenzione fosse "p" per "Potenza", ma sembra un bel modo di pensarci.
Mark Dickinson,

75

repr(e strin Python 3) pubblicherà tutte le cifre necessarie per rendere il valore inequivocabile. In questo caso il risultato della moltiplicazione 3*0.1non è il valore più vicino a 0,3 (0x1,3333333333333p-2 in esadecimale), in realtà è un LSB superiore (0x1,333333333333434p-2), quindi è necessario un numero maggiore di cifre per distinguerlo da 0,3.

D'altra parte, la moltiplicazione 4*0.1 fa ottenere il valore più vicino a 0,4 (0x1.999999999999ap-2 in esadecimale), quindi non necessita di ulteriori cifre.

Puoi verificarlo abbastanza facilmente:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

Ho usato la notazione esadecimale sopra perché è bella e compatta e mostra la differenza di bit tra i due valori. Puoi farlo tu stesso usando ad es (3*0.1).hex(). Se preferisci vederli in tutta la loro gloria decimale, ecco qui:

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')

2
(+1) Bella risposta, grazie. Pensi che valga la pena illustrare il punto "non il valore più vicino" includendo il risultato di 3*0.1 == 0.3e 4*0.1 == 0.4?
NPE,

@NPE Avrei dovuto farlo subito, grazie per il suggerimento.
Mark Ransom,

Mi chiedo se varrebbe la pena notare i valori decimali precisi dei "doppi" più vicini a 0,1, 0,3 e 0,4, dal momento che molte persone non riescono a leggere esagoni in virgola mobile.
supercat,

@supercat hai un buon punto. Mettere quei doppi super grandi nel testo sarebbe fonte di distrazione, ma ho pensato a un modo per aggiungerli.
Mark Ransom,

25

Ecco una conclusione semplificata da altre risposte.

Se controlli un float sulla riga di comando di Python o lo stampi, passa attraverso la funzione reprche crea la sua rappresentazione di stringa.

A partire dalla versione 3.2, Python stre reprusa uno schema di arrotondamento complesso, che preferisce piacevole-osservare decimali, se possibile, ma utilizza più cifre dove necessario garantire biunivoca (one-to-one) mapping tra carri e le loro rappresentazioni di stringa.

Questo schema garantisce che il valore di repr(float(s))sembra piacevole per i decimali semplici, anche se non possono essere rappresentati esattamente come float (ad es. Quando s = "0.1").

Allo stesso tempo garantisce che float(repr(x)) == xvale per ogni galleggiantex


2
La tua risposta è precisa per le versioni di Python> = 3.2, dove stre reprsono identiche per i float. Per Python 2.7, reprha le proprietà che identifichi, ma strè molto più semplice: calcola semplicemente 12 cifre significative e produce una stringa di output basata su quelle. Per Python <= 2.6, entrambi repre strsono basati su un numero fisso di cifre significative (17 per repr, 12 per str). (E a nessuno importa di Python 3.0 o Python 3.1 :-)
Mark Dickinson,

Grazie @MarkDickinson! Ho incluso il tuo commento nella risposta.
Aivar,

2
Si noti che l'arrotondamento dalla shell deriva reprquindi il comportamento di Python 2.7 sarebbe identico ...
Antti Haapala,

5

Non proprio specifico per l'implementazione di Python ma dovrebbe applicarsi a qualsiasi float per le funzioni di stringa decimale.

Un numero in virgola mobile è essenzialmente un numero binario, ma in notazione scientifica con un limite fisso di cifre significative.

L'inverso di qualsiasi numero che ha un fattore di numero primo che non è condiviso con la base si tradurrà sempre in una rappresentazione del punto punto ricorrente. Ad esempio 1/7 ha un fattore primo, 7, che non è condiviso con 10, e quindi ha una rappresentazione decimale ricorrente, e lo stesso vale per 1/10 con i fattori primi 2 e 5, quest'ultimo non è condiviso con 2 ; ciò significa che 0,1 non può essere rappresentato esattamente da un numero finito di bit dopo il punto.

Poiché 0,1 non ha una rappresentazione esatta, una funzione che converte l'approssimazione in una stringa di punti decimali generalmente tenterà di approssimare determinati valori in modo che non ottengano risultati non intuitivi come 0.1000000000004121.

Poiché il virgola mobile è in notazione scientifica, qualsiasi moltiplicazione per una potenza della base influisce solo sulla parte esponente del numero. Ad esempio 1.231e + 2 * 100 = 1.231e + 4 per la notazione decimale e similmente, 1.00101010e11 * 100 = 1.00101010e101 nella notazione binaria. Se moltiplico per una non-potenza della base, anche le cifre significative saranno influenzate. Ad esempio 1.2e1 * 3 = 3.6e1

A seconda dell'algoritmo utilizzato, potrebbe provare a indovinare i decimali comuni basandosi solo su cifre significative. Sia 0.1 che 0.4 hanno le stesse cifre significative in binario, perché i loro float sono essenzialmente troncamenti di (8/5) (2 ^ -4) e (8/5) (2 ^ -6) rispettivamente. Se l'algoritmo identifica il modello sigfig 8/5 come il decimale 1.6, allora funzionerà su 0,1, 0,2, 0,4, 0,8, ecc. Potrebbe anche avere modelli sigfig magici per altre combinazioni, come il float 3 diviso per float 10 e altri schemi magici statisticamente probabili che si formeranno per divisione per 10.

Nel caso di 3 * 0,1, le ultime cifre significative saranno probabilmente diverse dalla divisione di un galleggiante 3 per il galleggiante 10, facendo sì che l'algoritmo non riesca a riconoscere il numero magico per la costante 0,3 a seconda della sua tolleranza per la perdita di precisione.

Modifica: https://docs.python.org/3.1/tutorial/floatingpoint.html

È interessante notare che ci sono molti numeri decimali diversi che condividono la stessa frazione binaria approssimativa più vicina. Ad esempio, i numeri 0.1 e 0.10000000000000001 e 0.1000000000000000055511151231257827021181583404541015625 sono tutti approssimati di 3602879701896397/2 ** 55. Dal momento che tutti questi valori decimali condividono la stessa approssimazione, è possibile visualizzare uno qualsiasi di essi mantenendo comunque il valore invar ( ) == x.

Non c'è tolleranza per la perdita di precisione, se float x (0.3) non è esattamente uguale a float y (0.1 * 3), allora repr (x) non è esattamente uguale a repr (y).


4
Questo non aggiunge molto alle risposte esistenti.
Antti Haapala,

1
"A seconda dell'algoritmo utilizzato, potrebbe provare a indovinare i decimali comuni basandosi solo su cifre significative." <- Sembra pura speculazione. Altre risposte hanno descritto quello che Python in realtà fa.
Mark Dickinson,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.