Takie małe "czary-mary". Mogłoby się wydawać, iż sprawa powinna być oczywista dla informatyka, ale chyba przespałem wykłady na których była o tym mowa (wyszło szydło z worka...), i teraz poczułem się trochę zaskoczony natknąwszy się na wzmiankę o poniżej opisanych niedokładnościach w jednej z książek. Sprawa dotyczy operacji na ułamkach - w sumie ciekawa rzecz, poniżej zamieszczam kilka przykładów, o ile ktoś, podobnie jak ja, nie spotkał się jeszcze z problemem.
Zacznijmy od kodu w pythonie sumującego dziesięciokrotnie wartość 0.1 (na początku x przyrównujemy do 0 (zera), później w pętli, z każdym przebiegiem, dodajemy do x wartość 0.1):
x = 0.0 for i in range(10): x = x + 0.1 print (i,"%f" % x, x)
Wyniki, jak widać w składni funkcji "print" podzielone są na trzy "kolumny" - w pierwszej wypisujemy numer pętli (wartość "i"), w drugiej sformatowaną do postaci liczby zmiennoprzecinkowej o pojedynczej precyzji wartość "x", w trzeciej niesformatowaną wartość "x" - liczbę również zmiennoprzecinkową (rzeczywistą), ale o większej precyzji... "Czary" dzieją się oczywiście w kolumnie trzeciej. Jak widać już w trzecim przebiegu pojawia się jakiś "paproch" na dalekim miejscu po przecinku, później sytuacja wraca do normy, ale w iteracji ósmej niedokładności powracają...
(0, '0.100000', 0.1) (1, '0.200000', 0.2) (2, '0.300000', 0.30000000000000004) (3, '0.400000', 0.4) (4, '0.500000', 0.5) (5, '0.600000', 0.6) (6, '0.700000', 0.7) (7, '0.800000', 0.7999999999999999) (8, '0.900000', 0.8999999999999999) (9, '1.000000', 0.9999999999999999)
Formatowanie do pojedynczej precyzji (dla pythona) wydaje się niwelować problem przy tak małej ilości przebiegów pętli, ale nie dam sobie głowy uciąć, że go rozwiązuje :) Spójrzmy jeszcze na C# - najpierw typ "float":
using System; namespace testApp01 { class Program { static void Main(string[] args) { float x = 0.0f; for (int i = 1; i < 12; i++) { x = x + 0.1f; System.Console.WriteLine(x); } } } }
Poniżej wyniki, jak widać "paproch" pojawia się w okolicach ósmej iteracji...
0,1 0,2 0,3 0,4 0,5 0,6 0,7 0,8000001 0,9000001 1 1,1 Press any key to continue . . .
Zwiększymy precyzje, typ "double":
using System; namespace testApp01 { class Program { static void Main(string[] args) { double x = 0.0; for (int i = 1; i < 64; i++) { x = x + 0.1; System.Console.WriteLine(x); } } } }
Przy zwiększonej precyzji na "niedokładność" musimy poczekać trochę dłużej, ale również występuje:
5,4 5,5 5,6 5,7 5,8 5,9 5,99999999999999 6,09999999999999 6,19999999999999 6,29999999999999 Press any key to continue . . .
Słowem komentarza wspomną jeszcze o typie "Decimal" w C# (a raczej na platformie .Net Framework), tutaj niedokładności przy wielokrotnym sumowaniu ułamka 1/10 nie stwierdziłem, ale autorzy podręcznika o programowaniu w C#, który posiadam, nie są entuzjastyczni w odniesieniu do tego typu w kontekście omawianego problemu (w przeciwieństwie do niektórych stron internetowych), więc zalecam jednak ostrożność.
Ok, tak to ta cała informatyka potrafi niekiedy zaskoczyć. Przyczyna powyższych niedokładności jest dość ciekawa, otóż "trywialny" w systemie dziesiętnym ułamek 1/10 jest w systemie binarnym ułamkiem okresowym, co powoduje, iż jego wartość musi podlegać zaokrągleniom (podobnie jak w systemie dziesiętnym np. ułamek 1/3). Żadnych błędów nie ma - komputer oblicza wartości kolejnych sumowań najdokładniej jak potrafi, konieczność wykonywania zaokrągleń powoduje jednak pojawianie się niedokładności. I trzeba z tym żyć, rady nie ma... Dlatego, jeżeli piszecie program dla banku, przemyślcie proszę czy nie lepiej konwertować wartości do liczb całkowitych (stara szkoła - kwoty zapisywane w groszach, czyli zamiast 20.00 zł, 2000 gr) - to pozwoli uniknąć "paprochów" przy wykonywaniu, zdawałoby się trywialnych, np. sumowań :)))
I tak już zupełnie na koniec, Python przy C# (czy innym C podobnym języku) to jednak zgrabny jest... dużo mniej linijek, i jakoś tak "jaśniej"... Wcięcia zaczynają mnie chyba przekonywać...