كانت هذه التغريدة (انظر لقطة الشاشة) رائجة على تويتر (X) ولفتت انتباهي. إنها مثال آخر على الأخطاء في بايثون بسبب تفاصيل تنفيذها الفريدة.
نحن نعلم أن حسابات الفاصلة العائمة ليست دقيقة بسبب حدود تمثيلها. غالبًا ما تعمل اللغات الأخرى ضمنيًا على تعزيز int
إلى double
، مما يؤدي إلى مقارنات متسقة. ومع ذلك، فإن الأعداد الصحيحة ذات الدقة اللانهائية في بايثون تعقد هذه العملية، مما يؤدي إلى نتائج غير متوقعة.
يقارن بايثون القيمة الصحيحة بالتمثيل ذي الدقة المزدوجة للعدد العائم، والذي قد ينطوي على فقدان الدقة، مما يتسبب في هذه التناقضات. تتعمق هذه المقالة في تفاصيل كيفية إجراء CPython لهذه المقارنات، مما يوفر فرصة مثالية لاستكشاف هذه التعقيدات.
و هذا ما سنغطيه:
- مراجعة سريعة لتنسيق الدقة المزدوجة IEEE-754 — هكذا يتم تمثيل الأرقام ذات الفاصلة العائمة في الذاكرة.
- تحليل تمثيل IEEE-754 للأرقام الثلاثة.
- خوارزمية CPython لمقارنة الأعداد العائمة والأعداد الصحيحة.
- تحليل سيناريوهات الاختبار الثلاثة في سياق خوارزمية CPython.
مراجعة تنسيق IEEE-754
يستخدم CPython داخليًا نوع C double لتخزين قيمة النقطة العائمة، ونتيجة لذلك فإنه يتبع معيار IEEE-754 لتمثيل هذه القيم.
دعنا نراجع سريعًا ما تنص عليه المعايير بشأن تمثيل قيم الدقة المزدوجة. إذا كنت تعرف هذا بالفعل، فلا تتردد في الانتقال إلى القسم التالي.
يتم تمثيل قيم الدقة المزدوجة باستخدام 64 بت. من بين هذه:
- يتم استخدام بت واحد كبت إشارة
- يتم استخدام 11 بت لتمثيل الأس. يستخدم الأس تحيزًا قدره 1023، أي أن قيمة الأس الفعلية أقل بمقدار 1023 من القيمة في التمثيل ذي الدقة المزدوجة.
- ويتم استخدام 52 بت لتمثيل الجزء العشري
هناك الكثير من النظريات وراء هذا التنسيق، وكيف يتم تحويل الأرقام إليه وما هي أوضاع التقريب المختلفة – هناك ورقة كاملة حول هذا الموضوع يجب عليك قراءتها إذا كنت مهتمًا.
سأتناول الأساسيات، ولكن يجب أن تعلم أن هذا ليس التغطية الكاملة لتنسيق IEEE-754. سأحاول شرح كيفية تحويل الأرقام إلى تنسيق IEEE-754 في الحالات الشائعة عندما تكون > 1.
تحويل رقم الفاصلة العائمة إلى تنسيق IEEE-754
لتحويل رقم فاصل عشري إلى تمثيله الثنائي وفقًا لمعيار IEEE-754، يتعين عليك اتباع الخطوات الثلاث التالية. سننظر في مثال الرقم 4.5 لفهم كل خطوة من هذه الخطوات.
الخطوة 1: تحويل الرقم إلى ثنائي
يتطلب تحويل قيمة الفاصلة العائمة من الرقم العشري إلى الثنائي أن نقوم بتحويل الجزء الصحيح وجزء الكسر بشكل منفصل ثم في التمثيل الثنائي نربطهما بنقطة ثنائية (.).
التمثيل الثنائي للعدد 4 هو 100
وللعدد 0.5 هو 1
. وبالتالي، يمكننا تمثيل العدد 4.5 على أنه 100.1
.
الخطوة 2: تطبيع التمثيل الثنائي
الخطوة التالية هي تطبيع التمثيل الثنائي من الخطوة السابقة. التطبيع يعني ببساطة أن التمثيل الثنائي يجب أن يحتوي على بت واحد فقط قبل النقطة الثنائية.
في مثالنا، الرقم الثنائي هو 100.1
ولدينا 3 بتات قبل النقطة الثنائية. لتطبيعه، نحتاج إلى إجراء تحويل يمين بمقدار 2 بت. لذا فإن هذا التمثيل المتطبيع سيبدو مثل 1.001 * 2^2
.
الخطوة 3: استخراج العشري والأس من التمثيل الطبيعي
يعطينا التمثيل الطبيعي مباشرةً قيمة العشري والأس. في مثالنا الجاري، القيمة الطبيعية هي 1.001
. وللحصول على هذه القيمة، كان علينا تحريك القيمة الأصلية إلى اليمين بمقدار 2 بت، وبالتالي يصبح الأس هو 2.
في تنسيق IEEE-754، يكون الجزء العشري من الشكل 1.M. لا يشكل الرقم 1 الذي يسبق النقطة الثنائية جزءًا من التمثيل الثنائي لأنه معروف دائمًا أنه 1 وعدم تخزينه يمنحنا بتًا إضافيًا واحدًا من التخزين. وبالتالي، فإن الجزء العشري هنا هو 001
. ونظرًا لأن الجزء العشري يبلغ عرضه 52 بتًا، فإن البتات الـ 49 المتبقية ستكون أصفارًا.
التمثيل الثنائي النهائي
- بت الإشارة: الرقم موجب، لذا سيكون بت الإشارة 0.
- الأس: الأس هو 2. ومع ذلك، عند التمثيل بتنسيق IEEE-754، يلزم إضافة تحيز 1023 إليه، مما يعطينا 1025. لذا فإن القيمة الثنائية للأس هي
10000000001
. - الجزء العشري: الجزء العشري هو
001000…000
وأخيرًا، فيما يلي التمثيل الثنائي لـ IEEE-74 للرقم 4.5:
(0) (10000000001) (001000…000)
(لقد قمت بفصل المكونات المختلفة باستخدام الأقواس من أجل الوضوح.)
تحليل تمثيل IEEE-754 لحالات الاختبار الثلاثة
دعنا نستخدم سريعًا معرفتنا بتنسيق الدقة المزدوجة IEEE-754 لمعرفة تمثيل الأرقام الثلاثة من سيناريوهات الاختبار الثلاثة. سيكون هذا مفيدًا عندما نقوم بتحليلها في سياق خوارزمية CPython لمقارنة الأرقام العائمة.
تمثيل IEEE-754 لـ 9007199254740992.0
لنبدأ بالقيمة من السيناريو الأول: 9007199254740992.0
.
اتضح أن 9007199254740992
يساوي في الواقع 2^53
. وهذا يعني أنه في التمثيل الثنائي سيكون 1 متبوعًا بـ 53 صفرًا: 10000000000000000000000000000000000000000000000000000
.
سيكون الشكل الطبيعي 1.0 * 2^53
، والذي سيتم الحصول عليه عن طريق تحريك مكانالبتات 53 إلى اليمين، مما يمنحنا الأس 53.
تذكر أيضًا أن عرض الجزء العشري يبلغ 52 بتًا فقط، وأننا نقوم بنقل قيمتنا بمقدار 53 بتًا إلى اليمين، مما يعني أن الحد الأدنى للقيمة الأصلية سيضيع. ومع ذلك، نظرًا لأن جميع البتات هنا تساوي 0
، فلا يوجد أي ضرر وما زلنا قادرين على تمثيل 9007199254740992.0
بدقة.
دعونا نلخص مكونات 9007199254740992.0
:
- بت الإشارة: 0
- الأس:
53 + 1023
(التحيز) =1076
(10000110100
في النظام الثنائي) - مانتيسا:
0000000000000000000000000000000000000000000000000000
تمثيل IEEE-754 لـ 9007199254740993.0
القيمة من سيناريو الاختبار الثاني هي 9007199254740993.0
وهي أعلى بمقدار واحد من القيمة السابقة. ويمكن الحصول على تمثيلها الثنائي ببساطة عن طريق إضافة 1 إلى التمثيل الثنائي للقيمة السابقة:
10000000000000000000000000000000000000000000000000001.0
مرة أخرى، لتحويل هذا إلى الشكل الطبيعي، سيتعين علينا تحويله إلى اليمين بمقدار 53 بت، مما يعطينا الأس 53.
ومع ذلك، يبلغ عرض الجزء العشري 52 بت فقط. وعندما نحرك قيمتنا إلى اليمين بمقدار 53 بت، فسوف نفقد البت الأقل أهمية (LSB) الذي يحمل القيمة 1، مما يؤدي إلى أن يكون الجزء العشري كله أصفارًا. ويؤدي هذا إلى المكونات التالية:
- بت الإشارة: 0
- الأس:
53 + 1023
(التحيز) =1076
(10000110100
في النظام الثنائي) - مانتيسا:
0000000000000000000000000000000000000000000000000000
- تمثيل IEEE-754:
0100001101000000000000000000000000000000000000000000000000000000
لاحظ أن تمثيل IEEE-754 لـ 9007199254740993.0
هو نفس تمثيل 9007199254740992.0
. وهذا يعني أنه في تمثيله في الذاكرة، يتم تمثيل 9007199254740993.0
في الواقع على أنه 9007199254740992.0
.
يوضح هذا سبب إعطاء بايثون للنتيجة لـ 9007199254740993 == 9007199254740993.0
على أنها False، لأنه يرى ذلك كمقارنة بين 9007199254740993
و9007199254740992.0
.
تمثيل IEEE-754 لـ 9007199254740994.0
الرقم في سيناريو الاختبار الثالث والأخير هو 9007199254740994.0
، وهو أكبر بمقدار 1 من القيمة السابقة. ويمكن الحصول على تمثيله الثنائي عن طريق إضافة 1 إلى التمثيل الثنائي لـ 9007199254740993.0
، مما يعطينا: 10000000000000000000000000000000000000000000000000010.0
يحتوي هذا أيضًا على 54 بت قبل النقطة الثنائية ويتطلب تحويلًا إلى اليمين بمقدار 53 بتًا للتحويل إلى الشكل الطبيعي، مما يمنحنا الأس 53.
لاحظ أنه هذه المرة، تحتوي LSB الثانية على القيمة 1. وبالتالي، عندما نحول هذا الرقم إلى اليمين بمقدار 53 بت، فإنه يصبح LSB للجزء العشري (الذي يبلغ عرضه 52 بت).
تبدو المكونات هكذا:
- بت الإشارة: 0
- الأس:
53 + 1023 (bias) = 1076
(10000110100
in binary) - مانتيسا:
0000000000000000000000000000000000000000000000000001
- تمثيل IEEE-754:
0100001101000000000000000000000000000000000000000000000000000001
على عكس 9007199254740993.0
، يمكن لتمثيل IEEE-754 لـ 9007199254740994.0
أن يمثله بدقة دون أي فقدان للدقة. وبالتالي، فإن نتيجة 9007199254740994 == 9007199254740994.0
صحيحة في بايثون.
كيف يقوم CPython بتنفيذ مقارنة الأعداد العشرية (ذات الفاصلة العائمة)
إن تمثيل IEEE-754 للأرقام الثلاثة، وحقيقة أن بايثون لا يقوم بالترقية الضمنية للأعداد الصحيحة إلى أعداد مزدوجة، يوضحان النتيجة (النتائج) غير المتوقعة في سيناريوهات الاختبار من منشور Twitter.
ومع ذلك، إذا كنت تريد التعمق أكثر ورؤية كيفية قيام بايثون فعليًا بإجراء مقارنة بين الأعداد الصحيحة والأعداد العشرية، فتابع القراءة.
الدالة التي ينفذ فيها CPython المقارنة لكائنات float هي float_richcompare
والتي تم تعريفها في الملف floatobject.c.
إنها دالة كبيرة جدًا لأنها تحتاج إلى التعامل مع عدد كبير من الحالات. تُظهر لقطة الشاشة أعلاه التعليق الذي يشرح مدى صعوبة مقارنة الأعداد العشرية والأعداد الصحيحة. سأقوم بتقسيم الدالة إلى أجزاء أصغر ثم شرحها واحدة تلو الأخرى.
الجزء 1: مقارنة Float مقابل Float
تتميز لغة بايثون بنوع ديناميكي، مما يعني أنه عندما يتم استدعاء هذه الدالة بواسطة المترجم للتعامل مع عامل ==
، فلن يكون لديه أدنى فكرة عن الأنواع الملموسة للمتعاملات. تحتاج الدالة إلى معرفة الأنواع الملموسة والتعامل وفقًا لذلك. يتحقق الجزء الأول من دالة المقارنة مما إذا كان كلا الكائنين من أنواع float، وفي هذه الحالة تقارن ببساطة قيمة double الأساسية.
يوضح الشكل التالي الجزء الأول من الدالة:
لقد قمت بتسليط الضوء على الأجزاء المختلفة وترقيمها. دعونا نفهم ما يحدث، واحدًا تلو الآخر:
- يحمل كل من
i
وj
القيم المزدوجة الأساسية لـv
وw
، بينما يحملr
النتيجة النهائية لمقارنتهما. - عندما يتم استدعاء هذه الدالة بواسطة المترجم، يكون من المعروف بالفعل أن الوسيطة
v
هي من النوع double، ولهذا السبب لديهم شرط تأكيد لذلك. وهم يقومون بتعيين قيمة double الأساسية إلىi
. - بعد ذلك، يقومون بالتحقق مما إذا كان
w
أيضًا كائنًا عائمًا في بايثون، وفي هذه الحالة يمكنهم ببساطة تعيين قيمة double الأساسية إلىj
ومقارنتهما بشكل مباشر. - ولكن إذا لم يكن
w
عددًا عشريًا في بايثون، فإنهم يقومون بإجراء فحص مبكر للتعامل مع الحالة التي يكون فيهاv
له قيمة لا نهائية. في هذه الحالة، يقومون بتعيين القيمة0.0
إلىj
لأن القيمة الفعلية لـw
لا تهم عند المقارنة باللانهاية.
الجزء 2: مقارنة Float مقابل Long
إذا لم يكن w
عددًا عشريًا في بايثون بل عددًا صحيحًا، فستصبح الأمور مثيرة للاهتمام. تحتوي الخوارزمية على مجموعة من الحالات لتجنب إجراء مقارنة فعلية. سنغطي كل هذه الحالات واحدة تلو الأخرى.
تعالج الحالة الأولى الموقف عندما يكون لكل من v
وw
إشارات معاكسة. في هذه الحالة، ليست هناك حاجة لإجراء مقارنة بين القيم الفعلية لكل من v
وw
، وهو ما يفعله هذا الكود. توضح القائمة التالية الكود:
دعونا نفهم كل جزء مرقم من قائمة الكود:
- يتأكد هذا السطر من أن
w
هو كائن عدد صحيح في بايثون. - تحمل
vsign
وwsign
علامتيv
وw
على التوالي. - إذا كانت القيمتان لهما إشارات مختلفة، فلا داعي لمقارنة مقداريهما، يمكننا ببساطة استخدام الإشارات لاتخاذ القرار.
الجزء 2.1: w هو عدد صحيح ضخم
يتضمن الجزء التالي أيضًا حالة بسيطة، عندما تكون قيمة w
كبيرة جدًا بحيث تكون بالتأكيد أكبر من أي قيمة دقة مزدوجة (باستثناء اللانهاية التي تمت معالجتها مسبقًا). تُظهر القائمة التالية هذا الكود:
- تعيد
_PyLong_NumBits
عدد البتات المستخدمة لتمثيل كائن intw
. ومع ذلك، إذا كان كائن int كبيرًا جدًا بحيث لا يمكن لنوع size_t استيعاب عدد البتات فيه، فإنها تعيد -1 للإشارة إلى ذلك. - لذا، إذا كان
w
رقمًا كبيرًا، فمن المؤكد أنه أكبر من أي قيمة دقة مضاعفة. في هذه الحالة، المقارنة واضحة.
الجزء 2.2: تناسب w مع المزدوج
الحالة التالية بسيطة أيضًا. إذا كان بإمكان w
أن يتناسب مع نوع double، فيمكننا ببساطة تحويله إلى قيمة double ثم مقارنة قيمتي double. يوضح الجدول التالي الكود:
من الجدير بالذكر أنهم استخدموا 48 بتًا كمعيار لتحديد ما إذا كان ينبغي ترقية القيمة الصحيحة الأساسية لـ w إلى قيمة مزدوجة أم لا. لست متأكدًا من سبب اختيارهم 48 بتًا وليس 54 بتًا (وهو ما كان ليتجنب الفشل المذكور أعلاه) أو أي قيمة أخرى. ربما أرادوا التأكد تمامًا من عدم وجود فرصة لتجاوز الحد على أي منصة؟ لا أعرف.
الجزء 2.3: مقارنة الأسس
تم تصميم معظم هذه الحالات لتجنب مقارنة قيم int وfloat الفعلية. والحالة الأخيرة لتجنب مقارنة الأرقام الفعلية هي مقارنة أسسها. إذا كانت لها أسس مختلفة في شكلها الطبيعي، فيكفي مقارنتها. الرقم ذو الأس الأعلى يكون أكبر (بافتراض أن كلاً من v وw موجبان)
لاستخراج أس القيمة المزدوجة في v، يستخدم كود CPython دالة frexp من مكتبة C القياسية. تأخذ frexp قيمة مزدوجة x، وتقسمها إلى جزأين: قيمة الكسر المعيارية f، والأس e، بحيث x = f * 2 ^ e
. نطاق الكسر المعياري f
is [0.5, 1)
.
على سبيل المثال، لنأخذ القيمة 16.4. التمثيل الثنائي لها هو 10000.0110011001100110011
. لجعلها كسرًا طبيعيًا وفقًا لتعريف frexp، نحتاج إلى تحريكها إلى اليمين بمقدار 5 مواضع، مما يجعلها 0.100000110011001100110011 * 2^5
. وبالتالي، فإن قيمة الكسر الطبيعي هي 0.512500
والأس هو 5.
في حالة w، يكون عددًا صحيحًا، لذا فإن أسه هو ببساطة عدد البتات الموجودة فيه، أي nbits
. الآن، دعنا نلقي نظرة على الكود الخاص بهذه الحالة:
دعونا نلقي نظرة على كل جزء من الأجزاء المرقمة:
- أولاً، يجب التأكد من أن قيمة v موجبة.
- ثم يستخدمون دالة frexp لتقسيم i إلى أجزاء الكسر والأسس الطبيعية.
- بعد ذلك، يقومون بالتحقق مما إذا كان أس v سالبًا أو أصغر من أس w. في هذه الحالة، يكون v أصغر بالتأكيد من w.
- وإلا، إذا كان أس v أكبر من أس w، فإن v أكبر من w.
الجزء 2.4: كلا العددين لهما نفس الأسس
هذا هو الجزء الأخير والأكثر تعقيدًا في دالة المقارنة. والآن، كل ما تبقى هو مقارنة الرقمين فعليًا.
في هذه المرحلة، نعلم أن v وw لهما نفس الأس، مما يعني أنهما يستخدمان نفس عدد البتات لتمثيل قيمتيهما الصحيحة. لذا، لمقارنة v وw، يكفي مقارنة القيمة الصحيحة لـ v مع w.
لتقسيم v إلى أجزاء عددية صحيحة وكسرية، يستخدم كود CPython دالة modf من مكتبة C القياسية. على سبيل المثال، إذا كان الإدخال إلى modf هو 3.14، فإنه يقسمه إلى جزء عددي صحيح 3 وجزء كسري 0.14.
في أغلب الحالات، يكفي مقارنة القيم الصحيحة لـ v وw. ومع ذلك، لا يكفي ذلك عندما تكون قيمتيهما الصحيحة متساوية. على سبيل المثال، إذا كانت v=5.75 وw=5، فإن قيمتيهما الصحيحة تكون متساوية، ولكن v أكبر من w بالفعل. في مثل هذه الحالات، يجب أيضًا أخذ القيمة الكسرية لـ v في الاعتبار للمقارنة.
للقيام بذلك، يقوم كود CPython بحيلة بسيطة. إذا كان الجزء الكسري من v غير صفر، فسيتم تحويل قيمتي v وw الصحيحتين إلى اليسار بمقدار 1 وتعيين LSB لقيمة v الصحيحة إلى 1.
لماذا هذا التحول إلى اليسار وضبط LSB؟
- إن تحريك القيم الصحيحة لـ v وw إلى اليسار يؤدي ببساطة إلى ضربهما في 2. إذا كانت القيم الصحيحة لـ v وw متماثلة، فلن يغير هذا أي شيء. إذا كانت w أكبر من v، فستصبح أكبر قليلاً ولكن نتيجة المقارنة تظل كما هي.
- يؤدي ضبط LSB للقيمة الصحيحة v إلى زيادتها بمقدار 1. وتأخذ هذه الزيادة في الاعتبار القيمة الكسرية التي كانت جزءًا من v. في الحالة التي كانت فيها قيمتا v وw متساويتين في الأعداد الصحيحة، فبعد ضبط LSB، تكون قيمة v الصحيحة أكبر بمقدار 1 من w. ومع ذلك، إذا كانت w أكبر من v، فإن إضافة 1 إلى قيمة v الصحيحة لا تغير أي شيء.
- على سبيل المثال، إذا كانت v=5.75 وw=5 فإن قيمتهما الصحيحة هي 5. وإزاحتها إلى اليسار بمقدار 1 تجعلها 10. وضبط LSB لقيمة v الصحيحة سيجعلها 11 بينما تظل w عند 10. وبالتالي سنحصل على النتيجة الصحيحة لأن
11 > 10
. ومن ناحية أخرى، إذا كانت v=5.75 وw=6، فإن ضرب قيمتيهما الصحيحتين في 2 يعطي 10 و12 على التوالي. وإضافة 1 إلى قيمة v الصحيحة يجعلها 11. ومع ذلك تظل w أكبر ونحصل على النتيجة الصحيحة.
وفيما يلي قائمة لهذا الجزء من الكود:
وإليكم التفصيل:
- تحتوي fracpart و intpart على القيم الكسرية والصحيحة لـ v. vv و ww هما كائنات عددية صحيحة في بايثون (دقة لا نهائية) تمثل القيم الصحيحة لـ v و w.
- يتم استدعاء دالة modf لاستخراج الأجزاء الصحيحة والكسرية من v.
- يتناول هذا الجزء الحالة التي يكون فيها v مكون كسري غير صفري. يمكنك أن ترى أنهم قاموا بإزاحة vv وww إلى اليسار بمقدار 1، ثم قاموا بتعيين LSB لـ vv إلى 1.
- الآن، الأمر يتعلق فقط بمقارنة هاتين القيمتين الصحيحتين vv وww لتحديد قيمة الإرجاع.
العودة إلى تحقيقنا
استنادًا إلى كل المعرفة بمعيار IEEE-754 وكيفية قيام CPython بمقارنة الأعداد العائمة، دعنا نعود إلى سيناريوهات الاختبار ونتفكر في الناتج.
السيناريو 1: 9007199254740992 == 9007199254740992.0
>>> 9007199254740992 == 9007199254740992.
True
>>>
لدينا v=9007199254740992.0 و w=9007199254740992.
لقد توصلنا بالفعل إلى الجزء العشري والأس للعدد 9007199254740992.0. الأس هو 53 والجزء العشري هو 0 …
أيضًا، الرقم 9007199254740992 هو 2^53، مما يعني أنه يحتاج إلى 54 بت لتمثيله في الذاكرة (nbits=54)
دعونا نرى أي جزء من خوارزمية مقارنة التعويم سيتعامل مع هذا:
- w هو عدد صحيح لذا الجزء 1 لا ينطبق
- علامات v و w متماثلة لذا فإن الجزء الثاني لا ينطبق
- يتناسب w مع 54 بت، أي أنه ليس ضخمًا، لذا فإن الجزء 2.1 لا ينطبق
- يحتاج w إلى أكثر من 48 بت، لذا فإن الجزء 2.2 لا ينطبق
- الأس لكل من v وw في شكل الكسر الطبيعي هو 54، أي أن لهما أسس متساوية، لذا فإن الجزء 2.3 لا ينطبق أيضًا
- وهذا يقودنا إلى الجزء الأخير، الجزء 2.4. فلنبدأ في شرحه
نحن بحاجة إلى استخراج أجزاء العدد الصحيح والكسر من v، وإضافة واحد إلى الجزء الصحيح إذا كان جزء الكسر ليس صفراً، ثم مقارنة القيمة الصحيحة مع w.
في هذه الحالة، v=9007199254740992.0، يحتوي على جزء عدد صحيح 9007199254740992 وجزء كسري يساوي 0. القيمة الصحيحة لـ w هي أيضًا 9007199254740992. لذا، يقوم بايثون بإجراء مقارنة مباشرة بين العددين الصحيحين ويعيد النتيجة على أنها True.
السيناريو 2: 9007199254740993 == 9007199254740993.0
>>> 9007199254740993 == 9007199254740993.
False
>>>
كانت هذه نتيجة غير متوقعة. دعنا نحللها.
لدينا v=9007199254740993.0، وw=9007199254740993
كلتا القيمتين أكبر بمقدار 1 من القيم الموجودة في السيناريو السابق، أي v = w = 2^53 + 1.
في النظام الثنائي، 9007199254740993 يساوي 1000000000000000000000000000000000000000000000000000000000001.
تذكر أنه عندما كنا نبحث عن تمثيل IEEE-754 الخاص به، وجدنا أن هذا الرقم لا يمكن تمثيله بدقة وله نفس التمثيل مثل 9007199254740992.0.
لذا، بالنسبة إلى بايثون، تكون المقارنة فعليًا بين 9007199254740993 و9007199254740992.0. الآن، دعنا نرى ما يحدث داخل خوارزمية CPython.
تمامًا مثل السيناريو السابق، سنصل إلى الجزء الأخير من الخوارزمية (الجزء 2.4) حيث يتعين استخراج الجزء الصحيح والكسر من v، ثم يتعين مقارنة الجزء الصحيح بقيمة w.
داخليًا، يتم تمثيل 9007199254740993.0 في الواقع على هيئة 9007199254740992.0، لذا فإن الجزء الصحيح منه هو 9007199254740992. وفي الوقت نفسه، فإن قيمة w هي 9007199254740993. لذا فإن مقارنة هاتين القيمتين للحصول على المساواة تؤدي إلى نتيجة خاطئة.
السيناريو 3: 9007199254740994 == 9007199254740994.0
>>> 9007199254740994 == 9007199254740994.
True
>>>
على عكس السيناريو السابق، يمكن تمثيل 9007199254740994.0 بدقة باستخدام تنسيق IEEE-754. ومكوناته هي:
- بت الإشارة: 0
- الأس: 53 + 1023 (bias) = 1076 (10000110100 in binary)
- Mantissa:
0000000000000000000000000000000000000000000000000001
يؤدي هذا أيضًا إلى الجزء الأخير من الخوارزمية حيث يتعين مقارنة الأجزاء الصحيحة من v وw. في هذه الحالة، يكون لديهم نفس القيم وتؤدي مقارنة المساواة إلى True.
إن معيار IEEE-754 والحسابات ذات الفاصلة العائمة معقدة بطبيعتها، ومقارنة الأعداد ذات الفاصلة العائمة ليست بالأمر السهل. تطبق لغات مثل C وJava ترقية النوع الضمنية، وتحويل الأعداد الصحيحة إلى أعداد مزدوجة ومقارنتها شيئًا فشيئًا. ومع ذلك، لا يزال هذا من الممكن أن يؤدي إلى نتائج غير متوقعة بسبب فقدان الدقة.
تتميز لغة بايثون في هذا الصدد. فهي تحتوي على أعداد صحيحة ذات دقة لا نهائية، مما يجعل ترقية النوع غير ممكنة في العديد من المواقف. وبالتالي، تستخدم بايثون خوارزميتها المتخصصة لمقارنة هذه الأرقام، والتي لها حالات خاصة بها.
سواء كنت تستخدم لغة C أو Java أو Python أو لغة أخرى، فمن المستحسن استخدام وظائف المكتبة لمقارنة قيم الفاصلة العائمة بدلاً من إجراء مقارنات مباشرة لتجنب الأخطاء المحتملة.
لأكون صادقًا، لم أفكر مطلقًا في إلقاء نظرة على كيفية مقارنة الأعداد الصحيحة والعائمة باستخدام بايثون قبل هذه المقالة. والآن بعد أن ألقيت نظرة عليها، أشعر بمزيد من التنوير بشأن التعقيدات التي تجلبها الرياضيات ذات الفاصلة العائمة، وآمل أن تشعر أنت أيضًا بذلك!
اكتشاف المزيد من بايثون العربي
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.