View Categories

15.1. مقدمة

5 دقيقة

جدول المحتويات

15.1. مقدمة #

تُمثل الأعداد العشرية في أجهزة الحاسوب كسور ثنائية (أساسها 2). على سبيل المثال، قيمة الكسر العشري

0.125

لها القيمة 1/10 + 2/100 + 5/1000، وبالمثل قيمة الكسر الثنائي

0.001

هي 0/2 + 0/4 + 1/8. لهذين الكسرين قيمتان متطابقتان، والفرق الحقيقي الوحيد هو أن الأول مكتوب بنظام كسري أساسه 10، والثاني بنظام كسري أساسه 2.

لسوء الحظ، لا يمكن تمثيل معظم الكسور العشرية بدقة ككسور ثنائية. ونتيجة لذلك، فإن الأعداد العشرية التي تُدخلها عادةً ما تُقَرَّب فقط من خلال الأعداد الثنائية المخزنة فعليًا في الجهاز.

يبدو فهم المشكلة أسهل للوهلة الأولى في النظام العشري. لنأخذ الكسر 1/3. يمكنك تقريب ذلك ككسر أساسه ١٠:

0.3

أو، الأفضل،

0.33

أو، الأفضل،

0.333

وهكذا. مهما كان عدد الأرقام التي ترغب في كتابتها، فلن تكون النتيجة ١/٣ بالضبط، بل ستكون تقريبًا أفضل لـ ١/٣.

وبالمثل، مهما كان عدد أرقام الأساس ٢ التي ترغب في استخدامها، لا يمكن تمثيل القيمة العشرية ٠٫١ بالضبط ككسر أساسه ٢. في الأساس ٢، ١/١٠ هو الكسر المتكرر بلا حدود.

0.0001100110011001100110011001100110011001100110011...

توقف عند أي عدد محدود من البتات، وستحصل على تقريب. في معظم الأجهزة اليوم، تُقَرَّب الأعداد العشرية باستخدام كسر ثنائي، حيث يستخدم البسط أول 53 بت، بدءًا من البت الأكثر دلالة، والمقام قوةً للعدد 2. في حالة 1/10، يكون الكسر الثنائي 3602879701896397 / 2 ** 55، وهو قريب من القيمة الحقيقية لـ 1/10، ولكنه ليس مساويًا لها تمامًا.

لا يدرك العديد من المستخدمين هذا التقريب نظرًا لطريقة عرض القيم. تطبع بايثون فقط تقريبًا عشريًا للقيمة العشرية الحقيقية للتقريب الثنائي المُخزَّن في الجهاز. في معظم الأجهزة، إذا طبع بايثون القيمة العشرية الحقيقية للتقريب الثنائي المُخزّن لـ 0.1، فسيتعيّن عليه عرض

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

هذا عدد من الأرقام أكثر مما يراه معظم الناس مفيدًا، لذا يُبقي بايثون عدد الأرقام قابلاً للإدارة من خلال عرض قيمة مُقرّبة بدلاً من ذلك.

>>> 1 / 10
0.1

تذكر فقط، على الرغم من أن النتيجة المطبوعة تبدو كالقيمة الدقيقة لـ 1/10، فإن القيمة المُخزّنة الفعلية هي أقرب كسر ثنائي قابل للتمثيل.

ومن المثير للاهتمام أن هناك العديد من الأرقام العشرية المختلفة التي تشترك في نفس أقرب كسر ثنائي تقريبي. على سبيل المثال، الأرقام 0.1 و0.100000000000000001 و0.1000000000000000055511151231257827021181583404541015625 جميعها مُقرَّبة بالقيمة 3602879701896397 / 2 ** 55. بما أن جميع هذه القيم العشرية تشترك في نفس التقريب، يُمكن عرض أيٍّ منها مع الحفاظ على القيمة الثابتة eval(repr(x)) == x.

تاريخيًا، كان موجه بايثون ودالة repr() المُضمَّنة يختاران الرقم الذي يحتوي على 17 رقمًا معنويًا، وهو 0.100000000000000001. بدءًا من إصدار بايثون 3.1، أصبح بايثون (على معظم الأنظمة) قادرًا على اختيار أقصر هذه القيم وعرض 0.1 ببساطة.

لاحظ أن هذا جزء من طبيعة نظام الفاصلة العائمة الثنائي: هذا ليس عيبًا في بايثون، ولا في شفرتك البرمجية أيضًا. ستلاحظ نفس المشكلة في جميع اللغات التي تدعم حسابات الفاصلة العائمة في جهازك (مع أن بعض اللغات قد لا تعرض الفرق افتراضيًا، أو في جميع أوضاع الإخراج).

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

>>> format(math.pi, '.12g')  # give 12 significant digits
'3.14159265359'
>>> format(math.pi, '.2f')   # give 2 digits after the point
'3.14'
>>> repr(math.pi)
'3.141592653589793'

من المهم إدراك أن هذا، في الواقع، وهم: أنت ببساطة تُقرّب قيمة الآلة الحقيقية.

قد يُولّد وهمٌ آخر. على سبيل المثال، بما أن 0.1 ليس 1/10 بالضبط، فإن جمع ثلاث قيم للقيمة 0.1 قد لا يعطي 0.3 بالضبط أيضًا:

>>> .1 + .1 + .1 == .3
False

أيضًا، بما أن 0.1 لا يمكن أن يقترب من القيمة الدقيقة لـ 1/10، و0.3 لا يمكن أن يقترب من القيمة الدقيقة لـ 3/10، فإن التقريب المسبق باستخدام دالة round() لن يكون مفيدًا:

>>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
False

على الرغم من أنه لا يمكن تقريب الأرقام من قيمها الدقيقة المقصودة، إلا أن دالة round() يمكن أن تكون مفيدة للتقريب اللاحق بحيث تصبح النتائج ذات القيم غير الدقيقة قابلة للمقارنة مع بعضها البعض:

>>> round(.1 + .1 + .1, 10) == round(.3, 10)
True

تحمل حسابات الفاصلة العائمة الثنائية العديد من المفاجآت كهذه. مشكلة “0.1” موضحة بالتفصيل الدقيق أدناه، في قسم “خطأ التمثيل”. راجع “مخاطر الفاصلة العائمة” لمزيد من التفاصيل حول المفاجآت الشائعة الأخرى.

كما هو مذكور في النهاية، “لا توجد حلول سهلة”. مع ذلك، لا تكن حذرًا للغاية من الفاصلة العائمة! أخطاء عمليات الفاصلة العائمة في بايثون موروثة من برنامج بايثون، وفي معظم الأجهزة، لا تتجاوز جزءًا واحدًا من 2**53 لكل عملية. هذا أكثر من كافٍ لمعظم المهام، ولكن عليك أن تضع في اعتبارك أنها ليست عملية حسابية عشرية، وأن كل عملية حسابية عشرية قد تُعاني من خطأ تقريب جديد.

على الرغم من وجود حالات مرضية، إلا أنه في معظم الاستخدامات غير الرسمية لحسابات الفاصلة العائمة، سترى النتيجة المتوقعة في النهاية إذا قمت ببساطة بتقريب عرض نتائجك النهائية إلى عدد الأرقام العشرية المتوقعة. عادةً ما تكفي الدالة str()، وللتحكم بشكل أدق، راجع مُحددات التنسيق في دالة str.format() في “تنسيق سلسلة النصوص”.

لحالات الاستخدام التي تتطلب تمثيلًا عشريًا دقيقًا، حاول استخدام وحدة decimal التي تُطبّق الحساب العشري المناسب لتطبيقات المحاسبة والتطبيقات عالية الدقة.

تدعم وحدة الكسور (frans) شكلاً آخر من الحساب الدقيق، حيث تُطبّق الحساب بناءً على الأعداد النسبية (بحيث يُمكن تمثيل الأعداد مثل 1/3 بدقة).

إذا كنتَ مُستخدمًا مُكثّفًا لعمليات الفاصلة العائمة، فننصحك بالاطلاع على حزمة Numerical Python والعديد من الحزم الأخرى للعمليات الرياضية والإحصائية التي يُقدّمها مشروع SciPy. انظر <https://scipy.org>.

تُوفّر Python أدوات قد تُساعدك في الحالات النادرة التي تُريد فيها معرفة القيمة الدقيقة للعدد العائم. تُعبّر دالة float.as_integer_ratio() عن قيمة عدد عشري ككسر:

>>> x = 3.14159
>>> x.as_integer_ratio()
(3537115888337719, 1125899906842624)

بما أن النسبة دقيقة، يُمكن استخدامها لإعادة إنشاء القيمة الأصلية دون فقدان:

>>> x == 3537115888337719 / 1125899906842624
True

تُعبّر دالة float.hex() عن عدد عشري بالنظام الست عشري (الأساس 16)، مع إعطاء القيمة الدقيقة المُخزّنة في جهاز الكمبيوتر:

>>> x.hex()
'0x1.921f9f01b866ep+1'

هذه القيمة الدقيقة يمكن استخدام التمثيل السداسي عشري لإعادة بناء قيمة العدد العشري بدقة:

>>> x == float.fromhex('0x1.921f9f01b866ep+1')
True

بما أن التمثيل دقيق، فهو مفيد لنقل القيم بشكل موثوق عبر إصدارات مختلفة من بايثون (باستقلالية عن المنصة) وتبادل البيانات مع لغات أخرى تدعم التنسيق نفسه (مثل جافا وC99).

تُعد دالة math.fsum() أداة مفيدة أخرى تُساعد في الحد من فقدان الدقة أثناء الجمع. فهي تتتبع “الأرقام المفقودة” عند إضافة القيم إلى المجموع الجاري. يمكن أن يُحدث ذلك فرقًا في الدقة الإجمالية، بحيث لا تتراكم الأخطاء لدرجة تؤثر على الإجمالي النهائي:

sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True
error: Content is protected !!
Scroll to Top