2012-01-15 10 views
5

Poniższy kod Pythona oblicza liczbę iteracji, które należy wykonać na podstawie niektórych zmiennych.Arytmetyka zmiennoprzecinkowa: możliwe niebezpieczne uzależnienie od konkretnego porównania?

# a - b - c is always a multiple of d. 
    i = (a - b - c)/d 
    while i: 
    # do stuff 
    i -= 1 

Zmienne wszystko będzie tego samego typu, to znaczy tylko ints lub floats lub cokolwiek. Obawiam się, czy będzie działać poprawnie, jeśli wartości są floats. Wiem wystarczająco dużo, aby zawsze brać pod uwagę pułapki polegania na dokładnych wartościach zmiennoprzecinkowych. Ale nie mogę powiedzieć, czy powyższe jest niebezpieczne czy nie. Mogę użyć i = int(round((a - b - c)/d)), ale jestem ciekawy, jak lepiej zrozumieć pływaki.

Wszystko sprowadza się do następujących elementów: a - b - c jest dokładną wielokrotnością d. Więc polegam na (a-b-c)/d, aby stać się wartością i, którą mogę odjąć od 1 i uzyskać oczekiwaną liczbę iteracji w pętli while, z domniemanym założeniem, że i == 0 staje się prawdą. To znaczy, czy obliczone wielokrotności mogą być zmniejszane o 1, aby osiągnąć dokładnie 0?

Chciałbym nie tylko wiedzieć, czy jest to niebezpieczne, ale co ważniejsze, co muszę wiedzieć o zmiennoprzecinkowej, aby rozwiązać takie pytanie? Jeśli ktoś wie zdecydowanie, czy jest to bezpieczne czy nie, czy można wyjaśnić, w jaki sposób?

+0

Jeśli uważasz, że 'i' powinien mieć wartość całkowitą, rzuć ją do liczby całkowitej. To przynajmniej zagwarantuje, że twoja pętla się zakończy! – katrielalex

Odpowiedz

4

Można użyć decimal module, aby zorientować się, co „ukrywa” między liczbę zmiennoprzecinkową, takich jak 0.3:

>>> from decimal import Decimal 
>>> Decimal(0.3) 
Decimal('0.299999999999999988897769753748434595763683319091796875') 

Zauważ, że Python 2.7 zmienił sposób, w jaki są zapisywane liczby zmiennoprzecinkowe (jak działa repr(f)), tak aby teraz wyświetlał najkrótszy ciąg, który da tę samą liczbę zmiennoprzecinkową, jeśli wykonasz float(s). Oznacza to, że repr(0.3) == '0.3' w Pythonie 2.7, ale repr(0.3) == '0.29999999999999999' we wcześniejszych wersjach. Wspominam o tym, ponieważ może to jeszcze bardziej dezorientować, kiedy naprawdę chcesz zobaczyć, co kryje się za liczbami.

Korzystanie z modułu dziesiętną, widzimy błąd w obliczeniach z pływaków:

>>> (Decimal(2.0) - Decimal(1.1))/Decimal(0.3) - Decimal(3) 
Decimal('-1.85037170771E-16') 

Tutaj możemy spodziewać (2.0 - 1.1)/0.3 == 3.0, ale istnieje niewielka różnica niezerowe. Jeśli jednak zrobić obliczeń przy normalnych liczb zmiennoprzecinkowych, to rozumiem zera:

>>> (2 - 1.1)/0.3 - 3 
0.0 
>>> bool((2 - 1.1)/0.3 - 3) 
False 

Wynik zaokrągla się gdzieś po drodze od 1.85e-16 jest niezerowe:

>>> bool(-1.85037170771E-16) 
True 

Nie jestem pewien, gdzie dokładnie odbywa się to zaokrąglanie.

Co do zakończenia pętli w ogóle, to jest jedną wskazówkę można zaoferować: for floats less than 253, IEEE 754 can represent all integers:

>>> 2.0**53  
9007199254740992.0 
>>> 2.0**53 + 1 
9007199254740992.0 
>>> 2.0**53 + 2 
9007199254740994.0 

Przestrzeń pomiędzy numerami do zakodowania jest 2 od 2 do 2 , jak pokazano powyżej, . Ale jeśli twoja i jest liczbą całkowitą mniejszą niż 2 , wtedy i - 1 będzie również reprezentowalną liczbą całkowitą i ostatecznie trafisz 0.0, co w Pythonie jest uważane za fałszywe.

+0

Zaokrąglenie występuje w "Dziesiętnym (2.0) - dziesiętnym (1.1)", co daje "Dziesiętny (" 0.8999999999999999111821580300 ")', podczas gdy dokładna wartość 'podwójnego' wynosi' 0.899999999999999911182158029987476766109466552734375'. Użyj '(2.1 - 1.2)/0.3', aby uzyskać niezupełnie trzy wartości z' podwójnym's. –

3

Udzielę ci odpowiedzi agnostycznej na język (tak naprawdę nie znam Pythona).

Istnieje wiele potencjalnych problemów w kodzie. Po pierwsze, w ten sposób:

(a - b - c) 

Jeśli a jest (na przykład) 10 i b i c mają wartość 1, a następnie odpowiedź będzie 10 nie 10 -2 (l przy założeniu pływalności pojedynczej precyzji).

Następnie to jest:

i = (a - b - c)/d 

Jeżeli licznik, jak i mianownik są liczbami, które nie mogą być dokładnie przedstawione w zmiennoprzecinkowych (przykład 0,3 i 0,1), to wynikiem nie może być dokładnym całkowitą (to może być 3.0000001 zamiast 3). Dlatego twoja pętla nigdy się nie zakończy.

Potem jest tak:

i -= 1 

Podobnie jak wyżej, jeśli i jest obecnie 10 , to wynik tej operacji będzie nadal wynosić 10 , więc pętla nigdy nie wygaśnie.

Dlatego należy zdecydowanie rozważyć wykonanie wszystkich obliczeń w arytmetyce całkowitej.

+0

Tak, to pokazuje niewykonalność używania takich pływaków w najprostszy możliwy sposób. Sprawia, że ​​bardzo jasne, dzięki. – porgarmingduod

+0

@MartinGeisler: Tak, jednoznacznie sformułowałem to założenie (nie znam typów natywnych Pythona). Ale jak mówisz, wszystko, co naprawdę dzieje się z podwójną precyzją, to to, że wielkość się zmienia; wszystkie problemy wciąż istnieją. –

1

Masz rację, że może być brak zbieżności na zero (przynajmniej na więcej iteracji niż zamierzałeś). Dlaczego nie mieć twojego testu: while i >= 1. W takim przypadku, podobnie jak w przypadku liczb całkowitych, jeśli wartość i spadnie poniżej 1, pętla się zakończy.