15. Flytpunkts aritmetik: problem och begränsningar 7

flytpunktsnummer representeras i datormaskinvara som bas 2 (binära)fraktioner. Till exempel har decimalfraktionen

0.125

värde 1/10 + 2/100 + 5/1000, och på samma sätt har den binära fraktionen

0.001

värde 0/2 + 0/4 + 1/8. Dessa två fraktioner har identiska värden, den endaverklig skillnad är att den första är skriven i bas 10 fraktional notation och den andra i BAS 2.

tyvärr kan de flesta decimalfraktioner inte representeras exakt som binaryfraktioner. En konsekvens är att i allmänhet de decimala flytpunktnumren du anger endast approximeras av de binära flytpunktnumren som faktiskt lagras i maskinen.

problemet är lättare att förstå först i bas 10. Tänk på fraktionen1 / 3. Du kan approximera det som en bas 10 fraktion:

0.3

eller, bättre,

0.33

eller, bättre,

0.333

och så vidare. Oavsett hur många siffror du är villig att skriva ner, resultatetkommer aldrig att vara exakt 1/3, men kommer att bli en allt bättre approximation AV1/3.

På samma sätt, oavsett hur många bas 2-siffror du är villig att använda, kan det avgörande värdet 0.1 inte representeras exakt som en bas 2-fraktion. I base2 är 1/10 den oändligt upprepande fraktionen

0.0001100110011001100110011001100110011001100110011...

stopp vid något begränsat antal bitar, och du får en approximation. På mostmachines idag approximeras flottor med en binär fraktion med täljaren med de första 53 bitarna som börjar med den viktigaste biten ochmed nämnaren som en kraft på två. I fallet med 1/10, den binära fraktionenär 3602879701896397 / 2 ** 55 som ligger nära men inte exaktlika med det sanna värdet av 1/10.

många användare är inte medvetna om approximationen på grund av hur värdena visas. Python skriver bara ut en decimal approximation till den sanna decimalvärdet av den binära approximationen som lagras av maskinen. På de flesta maskiner skulle ifPython skriva ut det sanna decimalvärdet för den binära approximationen storedfor 0.1, Det skulle behöva visa

>>> 0.10.1000000000000000055511151231257827021181583404541015625

det är fler siffror än de flesta tycker är användbara, så Python håller numberof siffror hanterbara genom att visa ett avrundat värde istället

>>> 1 / 100.1

kom bara ihåg, även om det tryckta resultatet ser ut som det exakta värdet av 1/10, är det faktiska lagrade värdet närmaste representerbara binära fraktionen.

intressant är att det finns många olika decimaltal som delar sammaDen närmaste ungefärliga binära fraktionen. Till exempel är siffrorna 0.1 och0.10000000000000001 och0.1000000000000000055511151231257827021181583404541015625 allaungefärlig med 3602879701896397 / 2 ** 55. Eftersom alla dessa decimalvärden delar samma approximation, kan någon av dem visasmedan de fortfarande bevarar invarianten eval(repr(x)) == x.

historiskt sett skulle Python-prompten och den inbyggda repr()-funktionen väljaden med 17 signifikanta siffror, 0.10000000000000001. Börjar medpython 3.1, Python (på de flesta system) kan nu välja den kortaste ofthese och helt enkelt visa 0.1.

Observera att detta är i själva karaktären av binär flytpunkt: detta är inte en bugin Python, och det är inte heller ett fel i din kod. Du ser samma typ av sak på alla språk som stöder maskinvarans flytande punkträkning(även om vissa språk kanske inte visar skillnaden som standard eller i alloutput-lägen).

För mer behaglig utgång kanske du vill använda strängformatering för att producera ett begränsat antal signifikanta siffror:

>>> 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'

det är viktigt att inse att detta i verklig mening är en illusion: du avrundar helt enkelt visningen av det sanna maskinvärdet.

en illusion kan vara en annan. Till exempel, eftersom 0,1 inte är exakt 1/10,summerar tre värden på 0,1 kanske inte exakt 0,3, antingen:

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

också, eftersom 0,1 inte kan komma närmare det exakta värdet på 1/10 och0.3 kan inte komma närmare det exakta värdet på 3/10, sedan förrundning med

round()

funktionen kan inte hjälpa:

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

även om siffrorna inte kan göras närmare sina avsedda exakta värden,kan funktionenround() vara användbar för efterrundning så att resultatmed inexakta värden blir jämförbara med varandra:

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

binär flytande aritmetik har många överraskningar som denna. Problemetmed ” 0.1 ”förklaras i exakt detalj nedan, i avsnittet”Representationsfel”. Se farorna med Flytpunktför en mer fullständig redogörelse för andra vanliga överraskningar.

som det står nära slutet, ” det finns inga enkla svar.”Fortfarande, var inte otrevlig av flytpunkt! Fel i Python float-operationer är arvadefrån flytande hårdvara, och på de flesta maskiner är i storleksordningen nomer än 1 del i 2**53 per operation. Det är mer än tillräckligt för de flestauppgifter, men du måste komma ihåg att det inte är decimal aritmetik ochatt varje flottöroperation kan drabbas av ett nytt avrundningsfel.

medan patologiska fall existerar, för mest avslappnad användning av flytande punktaritmetisk ser du resultatet du förväntar dig i slutändan om du helt enkelt rundardisplay av dina slutliga resultat till antalet decimaltal du förväntar dig.str()vanligtvis räcker, och för finare kontroll se str.format() metodens formatspecifierare i Formatsträngsyntax.

för användningsfall som kräver exakt decimalrepresentation, försök användadecimal modul som implementerar decimal aritmetik lämplig förredovisningsapplikationer och applikationer med hög precision.

en annan form av exakt aritmetik stöds avfractions modulsom implementerar aritmetik baserat på rationella tal (så siffrorna som1 / 3 kan representeras exakt).

Om du är en tung användare av flyttalsoperationer bör du ta en titt på det numeriska Python-paketet och många andra paket för matematiska ochstatistiska operationer som tillhandahålls av SciPy-projektet. Se <https://scipy.org>.

Python tillhandahåller verktyg som kan hjälpa till vid de sällsynta tillfällen när du verkligen vill veta det exakta värdet av en flottör. Metodenfloat.as_integer_ratio() uttrycker värdet av en flottör som afraktion:

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

eftersom förhållandet är exakt kan det användas för att förlustfritt återskapa teoriginalvärde:

>>> x == 3537115888337719 / 1125899906842624True

float.hex() metod uttrycker en flottör i hexadecimal (base16), vilket återigen ger det exakta värdet som lagras av din dator:

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

denna exakta hexadecimala representation kan användas för att rekonstruerafloatvärdet exakt:

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

eftersom representationen är exakt är den användbar för att på ett tillförlitligt sätt portera värdeöver olika versioner av Python (plattformsoberoende) och utbytedata med andra språk som stöder samma format (som Java och C99).

ett annat användbart verktyg är math.fsum() funktion som hjälper mitigateloss-of-precision under summering. Den spårar ”förlorade siffror”som värden läggs till på en löpande total. Det kan göra skillnad i total noggrannhet så att felen inte ackumuleras till den punkt där de påverkarslutsumma:

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

15.1. Representationsfel kubi

detta avsnitt förklarar exemplet ”0.1” i detalj och visar hur du kan utföraen exakt analys av fall som detta själv. Grundläggande kännedom om binaryflytande punktrepresentation antas.

Representationsfel hänvisar till det faktum att vissa (de flesta, faktiskt)decimalfraktioner inte kan representeras exakt som binära (bas 2) fraktioner.Detta är den främsta anledningen till att Python (eller Perl, C, C++, Java, Fortran och manyothers) ofta inte visar det exakta decimaltalet du förväntar dig.

Varför är det? 1/10 är inte exakt representerbar som en binär fraktion. Nästan allamachines idag (November 2000) använder IEEE-754 flytande punkt aritmetik, ochnästan alla plattformar karta Python flyter till IEEE-754 ”dubbel precision”. 754dubblar innehåller 53 bitar av precision, så vidare inmatning strävar datorn efter attkonvertera 0,1 till närmaste fraktion Den kan av formen J / 2 * * N där J ärett heltal som innehåller exakt 53 bitar. Omskrivning

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

som

J ~= 2**N / 10

och erinrar om att J har exakt 53 bitar (är >= 2**52 men < 2**53),det bästa värdet för N är 56:

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

det vill säga 56 är det enda värdet för n som lämnar J med exakt 53 bitar. Det bästa möjliga värdet för J är då den kvoten avrundad:

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

eftersom resten är mer än hälften av 10 erhålls den bästa approximationengenom avrundning:

>>> q+17205759403792794

1/10 i 754 dubbel precision är:

7205759403792794 / 2 ** 56

att dela både täljaren och nämnaren med två minskar fraktionen till:

3602879701896397 / 2 ** 55

Observera att sedan vi avrundade, det här är faktiskt lite större än 1/10;om vi inte hade avrundat skulle kvoten ha varit lite mindre än1/10. Men i inget fall kan det vara exakt 1/10!

så datorn aldrig ”ser” 1/10: vad den ser är den exakta fraktionen givenovan, den bästa 754 dubbla approximationen det kan få:

>>> 0.1 * 2 ** 553602879701896397.0

om vi multiplicerar den fraktionen med 10**55 kan vi se värdet ut till55 decimaler:

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

vilket betyder att det exakta numret som lagras i datorn är lika med decimalvärdet 0.1000000000000000055511151231257827021181583404541015625.Istället för att visa hela decimalvärdet, många språk (inklusiveäldre versioner av Python), runda resultatet till 17 signifikanta siffror:

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

fractionsochdecimal moduler gör dessa 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'

Lämna ett svar

Din e-postadress kommer inte publiceras. Obligatoriska fält är märkta *