2.39. Praca z liczbami zmiennoprzecinkowymi¶
Liczby zmiennoprzecinkowe wyglądają jak zwykłe liczby dziesiętne, ale kilka ich zachowań zaskakuje osoby, które wcześniej się z nimi nie zetknęły – a jedno z tych zachowań jest bardziej widoczne w MicroPythonie niż w Pythonie na komputerze stacjonarnym. Ta strona omawia, czego się spodziewać i jak pisać kod operujący na liczbach zmiennoprzecinkowych tak, aby nie zachowywał się błędnie po cichu.
2.39.1. Precyzja¶
Pythonowy float to liczba zmiennoprzecinkowa binarna zgodna ze standardem IEEE 754. W większości wersji MicroPythona ma ona pojedynczą precyzję (32-bitową), podczas gdy CPython na komputerze stacjonarnym używa podwójnej precyzji (64-bitowej). Pojedyncza precyzja zapewnia około siedmiu cyfr dziesiętnych dokładności; podwójna – około piętnastu.
>>> 0.1 + 0.2
0.3000000
>>> 1.0 / 3.0
0.3333333
>>> 1e30 * 1e30
inf
Reprezentowalny zakres jest również węższy z obu stron: liczby o wartości bezwzględnej większej niż około 3.4e38 przepełniają się do inf, a liczby mniejsze niż około 1.2e-38 zaokrąglają się do zera.
2.39.2. Porównywanie liczb zmiennoprzecinkowych¶
Najczęstsza pułapka to sprawdzanie równości za pomocą ==:
>>> 0.1 + 0.2 == 0.3
False
Oba wyrażenia wyglądają, jakby powinny być równe, ale wynik 0.1 + 0.2 jest najbliższą reprezentowalną wartością, która nie jest dokładnie równa 0.3. Zamiast tego użyj sprawdzenia z tolerancją – pytaj, czy dwie liczby zmiennoprzecinkowe są wystarczająco bliskie, a nie identyczne:
if abs(a - b) < 1e-6:
# close enough
...
Wybór tolerancji zależy od skali wartości. Stała 1e-6 sprawdza się dobrze, gdy liczby są rzędu 1; względna tolerancja jest lepsza, gdy wartości różnią się o rzędy wielkości.
math.isclose() obsługuje oba przypadki naraz:
from math import isclose
isclose(0.1 + 0.2, 0.3) # True
isclose(1.0e6 + 1, 1.0e6) # True (within default tolerance)
Dwa argumenty nazwane kontrolują, który rodzaj „bliskości” się liczy:
rel_tol– tolerancja względna, domyślnie1e-9. Dwie wartości pasują do siebie, jeśli ich różnica mieści się w tym ułamku większej z nich. Dobre do ogólnych porównań w dowolnej skali.abs_tol– tolerancja bezwzględna, domyślnie0. Dwie wartości pasują do siebie, jeśli ich różnica mieści się w tej stałej wartości.
math.isclose() zwraca True, jeśli spełniona jest którakolwiek z tolerancji. Wartości domyślne są odpowiednie dla większości par liczb niezerowych; pułapka pojawia się, gdy jedna z wartości może być dokładnie zerem. Sprawdzenie tolerancji względnej sprowadza się do „różnica ≤ rel_tol × największa wartość”, a największa wartość wynosi zero, więc sprawdzenie zawsze zawodzi:
>>> isclose(0.0, 1e-12)
False
Sprawdzenie tolerancji bezwzględnej nie ma takiego problemu – przekaż abs_tol zawsze, gdy zero jest wartością, z którą możesz porównywać:
>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True
2.39.3. Kumulacja błędu¶
Długie sumy liczb zmiennoprzecinkowych tracą precyzję szybciej w MicroPythonie niż w CPythonie, ponieważ każdy wynik pośredni jest zaokrąglany z powrotem do precyzji 32-bitowej:
total = 0.0
for _ in range(1000000):
total += 0.1
print(total) # noticeably off from 100000.0
Dla powtarzanych dodawań, gdzie dokładność ma znaczenie, pomagają dwa wzorce:
Kumuluj w liczbie całkowitej, gdy tylko wartości można przeskalować do liczb całkowitych – pracuj w milisekundach zamiast w sekundach albo w miliwoltach zamiast w woltach, a następnie przelicz raz na końcu.
Obliczaj w mniejszych partiach i sumuj wyniki partii, tak aby każde dodawanie odbywało się między wartościami o podobnej wielkości.
Po stronie liczb całkowitych nie ma takiego ograniczenia – liczby całkowite w MicroPythonie mają dowolną precyzję, tak samo jak w CPythonie. Tam, gdzie masz wybór, preferuj arytmetykę całkowitoliczbową dla wszystkiego, gdzie utrata precyzji mogłaby się kumulować.