2.39. Liukulukujen käsittely

Liukuluvut näyttävät tavallisilta desimaaliluvuilta, mutta muutamat niiden käyttäytymisistä yllättävät lukijat, jotka eivät ole törmänneet niihin aiemmin – ja yksi näistä käyttäytymisistä on selvempi MicroPythonissa kuin pöytäkoneen Pythonissa. Tämä sivu käsittelee, mitä odottaa ja miten kirjoittaa liukulukukoodia, joka ei käyttäydy huomaamatta väärin.

2.39.1. Tarkkuus

Pythonin float on IEEE 754 -binääriliukuluku. Useimmissa MicroPython-käännöksissä se on yksinkertaista tarkkuutta (32-bittinen), kun taas pöytäkoneen CPython käyttää kaksinkertaista tarkkuutta (64-bittinen). Yksinkertainen tarkkuus kantaa noin seitsemän desimaalinumeron tarkkuuden; kaksinkertainen kantaa noin viisitoista.

>>> 0.1 + 0.2
0.3000000

>>> 1.0 / 3.0
0.3333333

>>> 1e30 * 1e30
inf

Esitettävissä oleva alue on myös kapeampi molemmista päistä: suuruudeltaan noin arvoa 3.4e38 suuremmat luvut ylivuotavat arvoon inf, ja noin arvoa 1.2e-38 pienemmät luvut pyöristyvät nollaan.

2.39.2. Liukulukujen vertailu

Yleisin ansa on yhtäsuuruuden testaaminen operaattorilla ==:

>>> 0.1 + 0.2 == 0.3
False

Molemmat lausekkeet näyttävät siltä, että niiden pitäisi olla yhtä suuret, mutta laskutoimituksen 0.1 + 0.2 tulos on lähin esitettävissä oleva arvo, joka ei ole tarkalleen 0.3. Käytä sen sijaan toleranssitarkistusta – kysy, ovatko kaksi liukulukua tarpeeksi lähellä toisiaan eikä identtisiä:

if abs(a - b) < 1e-6:
    # close enough
    ...

Toleranssin valinta riippuu arvojen mittakaavasta. Kiinteä 1e-6 toimii hyvin, kun luvut ovat suuruusluokkaa 1; suhteellinen toleranssi on parempi, kun arvot vaihtelevat suuruusluokittain.

math.isclose() käsittelee molemmat kerralla:

from math import isclose

isclose(0.1 + 0.2, 0.3)         # True
isclose(1.0e6 + 1, 1.0e6)       # True (within default tolerance)

Kaksi avainsana-argumenttia ohjaavat, minkälainen ”lähellä” lasketaan:

  • rel_tolsuhteellinen toleranssi, oletus 1e-9. Kaksi arvoa täsmäävät, jos niiden ero on tämän osuuden sisällä suuremmasta. Hyvä yleisiin vertailuihin missä tahansa mittakaavassa.

  • abs_tolabsoluuttinen toleranssi, oletus 0. Kaksi arvoa täsmäävät, jos niiden ero on tämän kiinteän määrän sisällä.

math.isclose() palauttaa arvon True, jos kumpi tahansa toleranssi täyttyy. Oletukset ovat sopivat useimmille nollasta poikkeavien lukujen pareille; ansa on, kun toinen arvoista voi olla tarkalleen nolla. Suhteellisen toleranssin tarkistus tulee muotoon ”ero ≤ rel_tol × suurin arvo”, ja suurin arvo on nolla, joten tarkistus epäonnistuu aina:

>>> isclose(0.0, 1e-12)
False

Absoluuttisen toleranssin tarkistuksessa ei ole tällaista ongelmaa – välitä abs_tol aina, kun nolla on arvo, johon saatat verrata:

>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True

2.39.3. Kertymäpoikkeama

Pitkät liukulukusummat menettävät tarkkuuden nopeammin MicroPythonissa kuin CPythonissa, koska jokainen välitulos pyöristetään takaisin 32-bittiseen tarkkuuteen:

total = 0.0
for _ in range(1000000):
    total += 0.1

print(total)        # noticeably off from 100000.0

Toistuviin yhteenlaskuihin, joissa tarkkuudella on väliä, kaksi mallia auttaa:

  • Kerää kokonaislukuun aina, kun arvot voidaan skaalata kokonaisluvuiksi – työskentele millisekunneissa sekuntien sijaan tai millivolteissa volttien sijaan, ja muunna sitten kerran lopussa.

  • Laske pienemmissä erissä ja summaa erien tulokset, jotta jokainen yhteenlasku on samansuuruisten arvojen välillä.

Kokonaislukupuolella ei ole tällaista rajaa – MicroPythonin kokonaisluvut ovat mielivaltaisen tarkkoja, aivan kuten CPythonin. Missä sinulla on valinnanvaraa, suosi kokonaislukuaritmetiikkaa kaikessa, missä tarkkuuden menetys kasaantuisi.