2.39. עבודה עם floats

מספרים בנקודה צפה נראים כמו מספרים עשרוניים רגילים, אך כמה מההתנהגויות שלהם מפתיעות קוראים שלא נתקלו בהן קודם – ואחת מאותן התנהגויות בולטת יותר ב-MicroPython מאשר ב-Python שולחני. עמוד זה מכסה למה לצפות וכיצד לכתוב קוד float שאינו מתנהג שלא כשורה בשקט.

2.39.1. דיוק

ה-float של Python הוא מספר בנקודה צפה בינארי לפי תקן IEEE 754. ברוב גרסאות הבנייה של MicroPython הוא דיוק יחיד (32-bit), בעוד CPython השולחני משתמש ב-דיוק כפול (64-bit). דיוק יחיד נושא כשבע ספרות עשרוניות של דיוק; כפול נושא כחמש עשרה.

>>> 0.1 + 0.2
0.3000000

>>> 1.0 / 3.0
0.3333333

>>> 1e30 * 1e30
inf

הטווח הניתן לייצוג גם צר יותר בשני קצותיו: מספרים גדולים מבערך 3.4e38 בעוצמתם גולשים ל-inf, ומספרים קטנים מבערך 1.2e-38 מתעגלים לאפס.

2.39.2. השוואת floats

המלכודת הנפוצה ביותר היא בדיקת שוויון באמצעות ==:

>>> 0.1 + 0.2 == 0.3
False

שני הביטויים נראים כאילו הם אמורים להיות שווים, אך התוצאה של 0.1 + 0.2 היא הערך הניתן לייצוג הקרוב ביותר, שאינו בדיוק 0.3. השתמשו ב-בדיקת סבילות במקום – שאלו אם שני floats קרובים מספיק במקום זהים:

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

בחירת הסבילות תלויה בקנה המידה של הערכים. 1e-6 קבוע עובד היטב כאשר המספרים בסדר גודל של 1; סבילות יחסית טובה יותר כאשר הערכים משתנים בסדרי גודל.

math.isclose() מטפלת בשניהם בבת אחת:

from math import isclose

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

שני ארגומנטי המילות מפתח שולטים באיזה סוג של ”קרוב“ נחשב:

  • rel_tol – סבילות יחסית, ברירת מחדל 1e-9. שני ערכים תואמים אם ההפרש ביניהם נמצא בתוך שבר זה של הגדול מביניהם. טוב להשוואות כלליות בכל קנה מידה.

  • abs_tol – סבילות מוחלטת, ברירת מחדל 0. שני ערכים תואמים אם ההפרש ביניהם נמצא בתוך כמות קבועה זו.

math.isclose() מחזירה True אם אחת מהסבילויות מתקיימת. ברירות המחדל מתאימות לרוב הזוגות של מספרים שאינם אפס; המלכודת היא כאשר אחד הערכים יכול להיות בדיוק אפס. בדיקת הסבילות היחסית מסתכמת ב-”הפרש ≤ rel_tol × הערך הגדול ביותר“, והערך הגדול ביותר הוא אפס, כך שהבדיקה תמיד נכשלת:

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

לבדיקת הסבילות המוחלטת אין בעיה כזו – העבירו abs_tol בכל פעם שאפס הוא ערך שאתם עשויים להשוות מולו:

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

2.39.3. סחיפת צבירה

סכומים ארוכים של floats מאבדים דיוק מהר יותר ב-MicroPython מאשר ב-CPython, מכיוון שכל תוצאת ביניים מעוגלת בחזרה לדיוק 32-bit:

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

print(total)        # noticeably off from 100000.0

עבור חיבורים חוזרים שבהם הדיוק חשוב, שני דפוסים עוזרים:

  • צברו לתוך מספר שלם בכל פעם שניתן להמיר את הערכים למספרים שלמים – עבדו במילישניות במקום בשניות, או במיליוולט במקום בוולט, ואז המירו פעם אחת בסוף.

  • חשבו באצוות קטנות יותר וסכמו את תוצאות האצווה, כך שכל חיבור הוא בין ערכים בעלי עוצמה דומה.

לצד המספרים השלמים אין מגבלה כזו – מספרים שלמים ב-MicroPython הם בדיוק שרירותי, בדיוק כמו אלה של CPython. במקומות שבהם יש לכם בחירה, העדיפו חשבון של מספרים שלמים עבור כל דבר שבו אובדן הדיוק יצטבר.