2.39. العمل مع الأعداد العشرية

تبدو أعداد الفاصلة العائمة كأعداد عشرية عادية، لكن بعض سلوكياتها تفاجئ القارئ الذي لم يصادفها من قبل -- وأحد تلك السلوكيات أكثر وضوحًا على MicroPython منه على Python سطح المكتب. تتناول هذه الصفحة ما ينبغي توقعه وكيفية كتابة شيفرة عشرية لا تسيء التصرف بصمت.

2.39.1. الدقة

نوع float في Python هو عدد فاصلة عائمة ثنائي وفق معيار IEEE 754. على معظم أبنية MicroPython يكون أحادي الدقة (32 بت)، بينما تستخدم CPython سطح المكتب مزدوج الدقة (64 بت). يحمل أحادي الدقة نحو سبعة أرقام عشرية من الدقة؛ ويحمل مزدوج الدقة نحو خمسة عشر رقمًا.

>>> 0.1 + 0.2
0.3000000

>>> 1.0 / 3.0
0.3333333

>>> 1e30 * 1e30
inf

المدى القابل للتمثيل أضيق أيضًا عند الطرفين: الأعداد الأكبر من نحو 3.4e38 في الحجم تفيض إلى inf، والأعداد الأصغر من نحو 1.2e-38 تُقرّب إلى الصفر.

2.39.2. مقارنة الأعداد العشرية

المزلق الأكثر شيوعًا هو اختبار التساوي باستخدام ==:

>>> 0.1 + 0.2 == 0.3
False

يبدو كلا التعبيرين كأنهما يجب أن يكونا متساويين، لكن نتيجة 0.1 + 0.2 هي أقرب قيمة قابلة للتمثيل، وهي ليست 0.3 بالضبط. استخدم بدلًا من ذلك فحص تسامح -- اسأل عما إذا كان عددان عشريان متقاربين بما يكفي بدلًا من كونهما متطابقين:

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. انجراف التراكم

الجموع الطويلة من الأعداد العشرية تفقد الدقة أسرع على MicroPython منها على CPython، لأن كل نتيجة وسيطة تُقرّب من جديد إلى دقة 32 بت:

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

print(total)        # noticeably off from 100000.0

للإضافات المتكررة حيث تهم الدقة، يساعد نمطان:

  • التراكم في عدد صحيح كلما أمكن قياس القيم إلى أعداد صحيحة -- اعمل بالميلي ثانية بدلًا من الثواني، أو بالميلي فولت بدلًا من الفولت، ثم حوّل مرة واحدة في النهاية.

  • الحساب على دفعات أصغر وجمع نتائج الدفعات، بحيث تكون كل عملية جمع بين قيم متقاربة في الحجم.

الجانب الصحيح لا يعاني من مثل هذا الحد -- أعداد MicroPython الصحيحة ذات دقة عشوائية، تمامًا مثل أعداد CPython. حيث يكون لديك الخيار، فضّل الحساب بالأعداد الصحيحة لأي شيء قد يتراكم فيه فقدان الدقة.