Martin Svoboda

Martin Svoboda - Blog

Podivné chování float čísel


V programování se často setkáváme s čísly typu float neboli s čísly s plovoucí desetinnou čárkou. Ačkoli se může zdát, že tato čísla fungují jako běžná desetinná čísla, jejich chování je v určitých situacích poměrně překvapivé. Tento příspěvek vysvětluje, proč se sčítání a násobení těchto čísel někdy chová jinak, než bychom očekávali.

Neočekávané výsledky s float čísly

Podívejme se nejprve na jednoduchý příklad:

print(0.1 + 0.1 + 0.1 == 0.3) # False
print(0.1 + 0.1 + 0.1 < 0.3)  # False
print(0.1 + 0.1 + 0.1 > 0.3)  # True

print(0.1 + 0.1 + 0.1)        # 0.30000000000000004
print(0.1 * 3)                # 0.30000000000000004

print(1/3)                    # 0.3333333333333333

Na první pohled by měl být výsledek 0.1 + 0.1 + 0.1 přesně 0.3, ale místo toho vidíme něco jako 0.30000000000000004. Je to překvapující? Ano. Ale proč se to děje?

Problém s přesností

Vše se odvíjí od toho, jak jsou čísla uložena v paměti počítače. Čísla float mají omezenou přesnost, což znamená, že některá desetinná čísla (jako 0,1) nelze přesně reprezentovat v binárním formátu, ve kterém počítače pracují.

Číslo je v počítači uloženo v paměti, která má konečnou velikost, tj. vejde se do něj pouze číslo konečné velikosti. Např. číslo 1, 2, 1000000000, 999516871 jsou konečná, ale ⅓ = 0.3333… má neukončený desetinný rozvoj a není možné jej celé uložit do paměti. Čísla, která se nevejdou do paměti jsou zaokrouhlena. Běžně lze uložit číslo, které obsahuje nanejvýš 17 platných číslic a určit polohu desetinné čárky. Float si lze představit jako celé číslo s maximální velikostí 17 číslic + určení polohy desetinné čárky. Běžně se tento stav ilustruje pomocí vědeckého zápisu. Ve vědeckém zápisu se čísla zapisují jako součin dvou částí: Koeficientu (nazývaného také signifikant nebo mantisa), což je obvykle číslo mezi 1 (včetně) a 10 (bez). Mocnina 10, která označuje polohu desetinné čárky.

Například:

  • 12345,67 = 1,234567 * 104
  • 0,00098 = 9,8 * 10-4

Zde je desetinná čárka “plovoucí”, protože může být umístěna kdekoli vzhledem k významným číslicím čísla a tato pozice je označena exponentem.

Tato flexibilita nám umožňuje reprezentovat velmi velká čísla, velmi malá čísla a čísla mezi nimi s využitím pevného velikosti paměti. Jak jsme však uvedli dříve, tato reprezentace s sebou nese určité problémy související s přesností.

Nejmenší kladná hodnota, kterou může float běžně nabývat je : 2,2250738585072014 * 10-308 a největší kladná hodnota: 1,7976931348623157 * 10308. Exponent pak určuje polohu

print(0.1 + 0.1 + 0.1) # 0.30000000000000004
print(0.1 + 0.2)       # 0.30000000000000004
print(0.1 * 2)         # 0.2
print(0.1 * 3)         # 0.30000000000000004
print(0.1 * 4)         # 0.4

Možná byste očekávali, že na výstupu bude 0,3, ale místo toho uvidíte něco jako 0,30000000000000004. Jste překvapeni? Je to způsobeno přirozenou nepřesností čísel s pohyblivou řádovou čárkou.

Důvodem je to, že čísla s pohyblivou řádovou čárkou jsou v hardwaru počítače reprezentována jako jedničky a nuly. Tedy ve zapsavá ve dvojkové soustavě. Mi používáme desitkovou soustavu. Problém je v tom, že některá čísla nemohou být reprezentována v dvojkové soustavě přesně.

Binární reprezentace

V desítkové soustavě je číslo 0.1 jednoduše reprezentovatelné jako jedna desetina. Ale ve dvojkové soustavě je toto číslo podobné zlomku 1/3 v desítkové soustavě — nekonečný periodický zlomek:

0.1 (desítkově) = 0.0001100110011001100110011… (dvojkově)

Vzor (0011) se opakuje donekonečna. Je to podobné jako u (1/3) v desítkové soustavě, kde se opakuje (3). Podobně je tomu u (0,2) ve dvojkové soustavě:0.210 = 0.001100110011001100110011…2. Když se pokusíš sečíst tyto dva opakující se binární zlomky v počítači, určitě dojde k zaokrouhlovacím chybám, protože počítač nemůže uložit nekonečný počet bitů. Musí tuto reprezentaci někde zkrátit nebo zaokrouhlit.

Dobře tohle vysvětluje proč print(0.1 + 0.2) nevypíše 0.3, ale jakto, že print(0.1) vypíše 0.1 a print(0.2) vypíše 0.2?

Když zadáte print(0.1) nebo print(0.2), Python interně používá funkci, která zobrazuje čísla s pohyblivou řádovou čárkou uživatelsky přívětivým způsobem. Pro účely zobrazení zaokrouhluje číslo na rozumný počet desetinných míst, takže vidíte to, co očekáváte: 0,1 nebo 0,2.

Interně je však zobrazení 0,1 a 0,2 z výše uvedených důvodů stále přibližné. Když nad těmito přibližnými hodnotami provádíte aritmetické operace, mohou být drobné nesrovnalosti zřetelnější a zobrazovací rutina je nemusí zaokrouhlit.

Jak porovnávat čísla typu float

Vzhledem k nepřesnosti, o které jsme hovořili, může porovnávání floatů někdy vést k neočekávaným výsledkům. Uvažujme následující:

print(0.1 + 0.1 + 0.1 == 0.3) # False
print(0.1 + 0.1 + 0.1 < 0.3) # False
print(0.1 + 0.1 + 0.1 > 0.3) # True

Při porovnávání čísel typu float z hlediska rovnosti je tedy obecně dobré považovat čísla za rovná, pokud jsou si dostatečně blízká, nikoliv za přesně rovná. Například můžete udělat něco takového:

epsilon = 0.00000001
print(abs(0.1 * 3 - 0.3) < epsilon) # True

Zde je epsilon malé číslo, které si zvolíte, a abs je funkce vracející absolutní hodnotu argumentu. Tento kód kontroluje, zda se čísla 0,1 * 2 a 0,3 nacházejí v rozmezí epsilon, což je spolehlivější způsob porovnávání čísel s pohyblivou desetinou čárkou na rovnost.

Řešení: Použití modulu decimal

Pokud potřebujete přesnou aritmetiku, například ve finančních výpočtech, můžete místo typu float použít modul decimal, který umožňuje pracovat s libovolnou přesností:

from decimal import Decimal

print(Decimal('0.1') + Decimal('0.2'))  # vypíše přesně 0.3

Závěr

Čísla s plovoucí desetinnou čárkou mohou někdy přinášet neočekávané výsledky kvůli omezením binární reprezentace. Pro většinu aplikací jsou však tyto nepřesnosti zanedbatelné. Pokud ale potřebujete větší přesnost, modul decimal je vaším pomocníkem.

Pamatujte, že tento problém není specifický pro Python, ale vyplývá z toho, jak počítače zpracovávají čísla s plovoucí desetinnou čárkou na hardwarové úrovni. Proto se s podobným chováním setkáte i v jiných programovacích jazycích.