ليه 0.1 + 0.2 ما بيساويش 0.3؟ الفاصلة العائمة من الصفر
المستوى: مبتدئ. المقال ده متكتب لحد لسه بادئ في البرمجة. أي مفهوم صعب هتلاقيه متشروح بمثال بسيط الأول، وبعدين بالتعريف العلمي الدقيق.
بعد المقال ده هتعرف بالظبط ليه الكمبيوتر بيقولك 0.1 + 0.2 = 0.30000000000000004، إزاي الغلط الصغير ده ممكن يضيّع قروش في كل فاتورة، وإزاي تمنعه بسطر واحد.
المشكلة باختصار
افتح أي كونسول بايثون أو جافاسكريبت دلوقتي واكتب 0.1 + 0.2. مش هيطلعلك 0.3. هيطلعلك 0.30000000000000004. ده مش عطل في اللغة ولا في جهازك. ده سلوك متعمّد وموجود في كل لغة بتشتغل بأرقام عشرية عادية: بايثون، جافاسكريبت، Java، C.
لو ماعرفتش السبب، هتفضل تكتب كود بيقارن أرقام عشرية بـ == وبيفشل من غير ما تفهم ليه. ولو بتحسب فلوس، الانحراف ده بيتراكم.
الحكاية بمثال بسيط الأول
تخيّل إنك عايز تكتب "ثلث" بالأرقام العشرية. تقسم 1 على 3، يطلعلك 0.3333333... والتلاتات دي مش بتخلص أبدًا. الورقة اللي بتكتب عليها محدودة، فلازم توقف عند حد وتقرّب. يعني إنت أصلًا مش بتكتب "ثلث" بالظبط، إنت بتكتب رقم قريب منه.
الكمبيوتر بيعاني من نفس المشكلة بالظبط، بس مع أرقام مختلفة. هو مابيفكّرش بنظام العشرة زينا، هو بيفكر بنظام الاتنين. وفي نظام الاتنين، الرقم 0.1 بيتحوّل لكسر دوري لا ينتهي، تمامًا زي الثلث عندنا.
دلوقتي الشرح العلمي الدقيق
الكمبيوتر بيخزّن الأرقام العشرية بصيغة اسمها الفاصلة العائمة مزدوجة الدقة (double precision floating point)، ومعرّفة في معيار عالمي اسمه IEEE 754. الرقم بيتخزّن في 64 بت متقسّمة تلات أجزاء: بت واحد للإشارة، 11 بت للأس، و52 بت للأرقام نفسها (المانتيسا).
الـ 52 بت دول بيدّوك دقة حوالي 15 إلى 17 خانة عشرية بس. أي حاجة بعد كده بتتقرّب. المشكلة إن 0.1 في النظام الثنائي محتاج عدد لا نهائي من البتات عشان يتكتب بالظبط. فالكمبيوتر بيقرّبه لأقرب رقم يقدر يخزّنه. نفس الكلام مع 0.2. فلما تجمع رقمين مقرّبين، الغلط الصغير في كل واحد بيظهر في الناتج على شكل ...04 في الآخر.
جرّبها بنفسك بكود شغّال
في بايثون:
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
# جمع 0.1 عشر مرات المفروض يطلع 1.0
>>> sum(0.1 for _ in range(10))
0.9999999999999999
في جافاسكريبت نفس الحكاية:
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
سيناريو واقعي: الفلوس
ركز في الحتة دي، لأنها اللي بتوجع فعلًا. لو عندك سيستم كاشير بيجمع أسعار، وكل صنف بـ 0.10 جنيه، ودخل 10 أصناف، إنت متوقع 1.00 جنيه. بالـ float هيطلعلك 0.9999999999999999. لوحده الفرق تافه. بس لو السيستم بيعمل ملايين العمليات في اليوم، ممكن تخسر مبالغ حقيقية على مدار الشهر.
الحل الأول: استخدم Decimal في بايثون، اللي بيحسب بالنظام العشري بالظبط:
from decimal import Decimal
Decimal("0.1") + Decimal("0.2") # النتيجة: Decimal('0.3') بالظبط
الحل التاني والأفضل للفلوس: خزّن كل حاجة بالقروش كأعداد صحيحة. الأعداد الصحيحة دقيقة 100% في الجمع والطرح:
total_qirsh = 10 + 10 + 10 # 30 قرش، دقيق تمامًا
pounds = total_qirsh / 100 # 0.30 للعرض بس
وللمقارنة بين رقمين عشريين؟
متقارنش رقمين عشريين بـ == أبدًا. قارن الفرق بينهم بهامش صغير جدًا اسمه إبسيلون:
EPS = 1e-9
if abs((0.1 + 0.2) - 0.3) < EPS:
print("متساويين عمليًا") # ده اللي هيطبع
في جافاسكريبت فيه ثابت جاهز اسمه Number.EPSILON بيعمل نفس الفكرة.
الـ trade-off: تكسب إيه وتخسر إيه
الـ float سريع جدًا وكفء في الذاكرة. ده اختيار ممتاز للرسوميات والفيزياء والحسابات العلمية. المكسب: سرعة. الخسارة: دقة عشرية مش مضمونة. الـ Decimal دقيق تمامًا بس أبطأ من float بحوالي 10 لـ 100 مرة حسب العملية. الأعداد الصحيحة (القروش) هي الأسرع والأدق للفلوس.
متى لا تستخدم float
ابعد عن float نهائيًا في: الفلوس والمحاسبة، أي مقارنة مساواة دقيقة، وأي حساب بيتراكم فيه الغلط عبر ملايين العمليات. في المقابل، float تمام في الحسابات العلمية وإحداثيات الرسم والنِسب اللي مش محتاجة دقة مطلقة.
الخطوة التالية
افتح كونسول بايثون دلوقتي واكتب sum(0.1 for _ in range(10)) وشوف الناتج بعينك. وبعدين لو مشروعك فيه أي فلوس متخزّنة كـ float، حوّلها لقروش صحيحة أو Decimal قبل ما تلاقي فرق في حساباتك ماتعرفش منين جه.
المصادر
- معيار IEEE 754-2019 للحساب بالفاصلة العائمة.
- توثيق بايثون الرسمي: Floating Point Arithmetic: Issues and Limitations — docs.python.org/3/tutorial/floatingpoint.html
- David Goldberg, "What Every Computer Scientist Should Know About Floating-Point Arithmetic", ACM Computing Surveys, 1991.
- توثيق MDN حول Number.EPSILON — developer.mozilla.org
- توثيق بايثون لوحدة decimal — docs.python.org/3/library/decimal.html