Subsections

 
B. Artitmetika v plovoucí řádové čárce: Problémy a jejich náprava

Tato kapitola se věnuje problematice čísel v plovoucí řádové čárce. Vysvětluje specifika tohoto datového typu, která se týkají nejen jazyka Python. Také vysvětluje, jakým způsobem přistupuje Python k reálným číslům.

Jak jistě dobře víte, v počítači jsou čísla uložena ve dvojkové soustavě (tj. číselné soustavě o základu 2). Stejně jsou uložena i desetinná čísla. Ta jsou reprezentována jako zlomky se jmenovatelem, jež je roven mocnimám čísla 2. Například desetinné číslo

0.125

má hodnotu 1/10 + 2/100 + 5/1000. Jde stejné číslo reprezentuje i binární zápis

0.001

Tento zápis znamená 0/2 + 0/4 + 1/8. Oba zápisy reprezentují stejné číslo, ovšem pokaždé zapsané v jiné soustavě.

Bohužel mnoho desetinný čísel zapsaných v desítkové soustavě nemá odpovídající binární tvar. Proto jsou tato čásla uložena pouze jako přibližná hodnota.

Tento problém si demonstrujeme na zlomku jedna třetina. V desítkové soustavě ho můžeme napsat pouze přibližně jako desetinné číslo:

0.3

nebo lépe:

0.33

nebo ještě lépe:

0.333

a tak dále. Nezáleží na tom, kolik číslic napíšete, výsledek nikdy nebude přesně 1/3, vždy půjde o přesnější a přesnější aproximaci čísla 1/3.

Podobně je ve dvojkové soustavě periodická jedna desetina. Vždy půjde nekonečný periodický rozvoj

0.0001100110011001100110011001100110011001100110011...

Použitím pouze konečného počtu číslic získáme pouze přibližnou hodnotu. Díky tomu můžeme o interpretru získat výsledky podobně tomuto:

>>> 0.1
0.10000000000000001

Jak vidíte, Python vytiskl číslo 0.1 tak, jak ho on "vidí". Na některých platformách můžeme získat i jiný výsledek. Jednotlivé stroje se totiž mezi sebou (kromě jiného) liší i počtem bitů, které používají pro aritmetiku v plovoucí řádové čárce. Proto nás nesmí překvapit i aproximace podobná této:

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

Interaktivní interpretr Pythonu používá pro získání řetězcové reprezentace nějaké hodnoty interní funkce repr(). Ta desetinná čásla zaoukrouhluje na 17 platných cifer.B.1 Tato cifra byla vypočítána programátory Pythonu, pokud by použili 16 cifer, nebyla by již výše uvedená podmínka platná pro všechna reálná čísla.

Zapamatujte si, že problém s reprezentací reálných čísel není problémem Pythonu. Také nejde o chybu ve vašem kódu. Se stejnými problémy se setkáte i v jiných jazycích, které používají klasické matematické funkce. Tyto jazyky však mohou být napsány tak, že rozdíly mezi jednotlivými reprezentacemi nemusí implicitně zobrazovat.

Oproti funkci repr() zde existuje i interní funkce str(), která vrací reálné číslo s přesností na 12 platných číslic. V některých případech ji proto můžete používat namísto funkce repr(), poněvadž její výstup vypadá v mnoha případech lépe:

>>> print str(0.1)
0.1

Musíte mít na paměti, že to co vidíte je jakási "iluze", hodnota ve skutečnosti není přesně 1/10, ale její aproximace zaokrouhlená na 12 platných číslic.

Můžete se dočkat i dalších překvapení. Pokud nepoučený programátor napíše 0.1 a uvidí následující výsledek

>>> 0.1
0.10000000000000001

může se pokusit výsledek zaokrouhlit pomocí funkce round(). Ta ale neprovede žádné zaokrouhlení!

>>> round(0.1, 1)
0.10000000000000001

V čem je problém? Binární desetinné číslo s hodnotou 0.1 je uloženo jako nejlepší aproximace zlomku 1/10. Pokusíte-li se jí nyní zaokrouhlit, lepší aproximaci již nezískáte!

Jiné překvapení -- 0.1 není přesně 1/10! Sečtete-li tedy desetkrát číslo 0.1, nedostanete 1.0:

>>> sum = 0.0
>>> for i in range(10):
...     sum += 0.1
...
>>> sum
0.99999999999999989

Binární reprezentace reálných čísel obnáší ještě další podobné problémy. O některých jejich důsledcích a nápravách se dočtete na dalších řádcích. Kompletní výčet možných "chyb" najdete na adrese http://www.lahey.com/float.html.

Uvědomte si, že na tyto problémy nikdy neexistuje jednoznačná náprava. Aritmetiky v plovoucí řádové čárce se ale bát nemusíte! Chyby v plovoucí řádové čárce jsou závislé na použitém počítači a jejich výskyt je poměrně sporadický. To je pro většinu úloh naprosto postačující okolnost.

Vždy se ale snažte jednotlivé operace skládat tak, aby nedocházelo ke zvětšování chyby. Konkrétní postup se liší úlohu od úlohy. Pokud jste již zkušenější programátor, jistě si dokážete sami pomoci. A začátečníky nezbývá než odkázat na příslušnou literaturu či internetové stránky.

 
B.1 Chyby v reprezentaci čísel

Této podkapitole se blíže podíváme na to, jak jsou v počítači interně ukládána reálná čísla a jakým způsobem se vyvarovat většině problémů spojených s používáním reálných čísel na dnešních počítačových platformách.

Chyba v reprezentaci čísel znamená, že některá desetinná čísla v desítkové soustavě přesně neodpovídají binárním desetinným číslům. To je důvod proč někdý Python (nebo Perl, C, C++, Java, Fortran a další jazyky) nezobrazí přesně to číslo, které jste očekávali:

>>> 0.1
0.10000000000000001

V čem je problém? Jak jsme si již řekli, číslo 0.1 nemá v binární soustavě odpovídající zápis. V dnešní době téměř všechny počítače používají reálnou aritmetiku podle standardu IEEE-754 a téměř na všech platformách odpovídá Pythonovský datový typ float číslu s dvojitou přesností.

Tato čísla obsahují 53 bitů určených k reprezentaci hodnoty čísla (matisa). Při ukládání reálné hodnoty se tato v počítači nejprve přetransformuje na zlomek ve tvaru J/2**N, kde J je celé číslo obsahující přesně 53 bitů. Přepíšeme-li

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

jako

J \~= 2**N / 10

a za předpokladu, že J má přesně 53 bitů (tj. platí >= 2**53 ale < 2**53), je nejlepší hodnotou pro N 56.

>>> 2L**52
4503599627370496L
>>> 2L**53
9007199254740992L
>>> 2L**56/10
7205759403792793L

56 je jediná hodnota N, která zajistí, že J je dlouhá přesně 53 bitů. Nejlepší možnou hodnotu J získáme zaokrouhlením:

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

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

Jelikož je zbytek větší než polovina z 10, nejlepší aproximaci získáme zaokrouhlením nahoru:

>>> q+1
7205759403792794L

Takže nejlepší možnou reprezentací 1/10 v IEEE-754 typu double precision je tato hodnota vydělená 2**56, tedy

7205759403792794 / 72057594037927936

Protože jsme zaokrouhlovali nahoru, je výsledek o trochu větší než 1/10. Pokud bychom zaokrouhlení neprovedli, výsledek by byl trochu menší než 1/10. Ale nikdy nebude přesně 1/10!

Takže počítače nikdy "neuvidí" 1/10: vždy bude používat zlomek 7205759403792794 / 72057594037927936:

>>> .1 * 2L**56
7205759403792794.0

Vynásobíme-li tuto hodnotu číslem 10**30, uvidíme hodnotu jejích 30 nejvýznamnějších dekadických číslic:

>>> 7205759403792794L * 10L**30 / 2L**56
100000000000000005551115123125L

Číslo uložené v počítači je tedy přibližně rovno dekadickému číslu 0.100000000000000005551115123125. Zaokrouhlením na 17 platných číslic získáme právě onu hodnotu 0.10000000000000001, kteroužto Python zobrazuje namísto hodnoty 0.1!



Footnotes

... cifer.B.1
17 cifer je použito aby byla splněna rovnost eval(repr(x)) == x.
Viz O tomto dokumentu... kde naleznete informace, jak upozornit na případné chyby.