الأعداد العشرية decimal في بايثون

من موسوعة حسوب

تتيح وحدة decimal إجراء حسابات سريعة على الأعداد العشرية مع ضمان التقريب الصحيح.

>>> import decimal
>>> Decimal(10)
Decimal('10')
>>> Decimal('3.14')
Decimal('3.14')
>>> Decimal(3.14)
Decimal('3.140000000000000124344978758017532527446746826171875')

ميزات الوحدة decimal

تتفوق هذه الوحدة على نوع الأعداد العشرية float بعدة ميزات:

  • العدد العشري decimal "يستند إلى نموذج أعداد عشرية ذات فاصلة عائمة يراعي الاستخدام البشري ويلتزم بمبدأ أساسي هو أنّه يجب أن توفّر الحواسيب عمليات حسابية تعمل بنفس الطريقة التي يتعلّمها الناس في المدارس" - اقتباسٌ من مواصفات العمليات الحسابية التي تجرى على الأعداد العشرية decimal.
  • يمكن تمثيل الأعداد العشرية decimal بدقّة، أما الأرقام مثل 1.1 و 2.2 فلا تمثل تمثيلًا دقيقًا في النظام الثنائي للأعداد ذات الفاصلة العائمة. ولا يتوقع المستخدم أن تعطي العملية الحسابية ‎1.1 + 2.2‎ النتيجة 3.3000000000000003 كما هو الحال مع الأعداد في النظام الثنائي للأعداد ذات الفاصلة العائمة.
  • تحافظ الأعداد العشرية decimal على الدقة عند إجراء العمليات الحسابية، ففي هذا النظام يكون ناتج العمليات التالية: ‎0.1 + 0.1 + 0.1 - 0.3 هو الصفر، أما في النظام الثنائي للفاصلة العائمة يكون الناتج 5.5511151231257827e-017. صحيح أن هذا الرقم قريب جدًّا من الصفر، ولكن تعيق هذه الفروقات إجراء عمليات مقارنة دقيقة للمساواة إلى جانب أنّ هذه الفروقات يمكن أن تتزايد مع الاستمرار في إجراء العمليات الحسابية؛ لذا يفضل استخدام النوع decimal في التطبيقات المحاسبية التي تتطلّب إجراء مقارنات مساواة صارمة.
  • تعتمد وحدة decimal مفهوم الأعداد المعنوية (significant places)؛ لذا فإنّ ناتج العملية ‎1.30 + 1.20 هو 2.50، إذ تحتفظ اللغة بالصفر الأخير إشارة إلى أنّه عدد معنوي. مفهوم الأعداد المعنوية هو من المفاهيم الشائعة لتمثيل الأرقام في التطبيقات المالية والمحاسبية. وفي عملية الضرب تستخدم جميع الأرقام للحصول على الناتج، فعلى سبيل المثال تعطي العملية ‎1.3 * 1.2 الناتج 1.56 أما العملية ‎1.30 * 1.20 فتعطي الناتج 1.5600.
  • على عكس الأعداد العشرية ذات الفاصلة العائمة، يمكن تحديد مقدار الدقة المعتمدة في الأعداد العشرية decimal من قبل المستخدم (القيمة الافتراضية هي 28 مرتبة) ويمكن استخدام أي عدد مهما كان كبيرًا، مثال:
>>> from decimal import *
>>> getcontext().prec = 6
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')
>>> getcontext().prec = 28
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')
  • تستخدم الأعداد العشرية بنوعيها float و decimal في بايثون بالاعتماد على المعايير المعروفة، ويعرض النوع float جزءًا بسيطًا من هذه المعايير، في حين أنّ وحدة decimal تعرض جميع الأجزاء المطلوبة ضمن هذه المعايير. ويمكن للمبرمج - عند الحاجة - أن يسيطر بصورة تامة على عمليات التقريب والتعامل مع الإشارات signals، ويمكن كذلك فرض عمليات حسابية دقيقة ومضبوطة باستخدام استثناءات توقف تنفيذ أي عملية حسابية غير دقيقة.
  • صمّمت وحدة decimal لتدعم كلًّا من العمليات الحسابية على الأرقام العشرية غير المقرّبة الدقيقة (تسمى في بعض الأحيان العمليات الحسابية على الأعداد ذات الفاصلة الثابتة [fixed-point arithmetics]) والعمليات الحسابية على الأعداد ذات الفاصلة العائمة المقرّبة.

يتمحور تصميم وحدة decimal على ثلاثة مفاهيم أساسية: العدد العشري، والسياق (context) الخاص بالعملية الحسابية، والإشارة (signal).

العدد العشري غير قابل للتغيير (immutable)، ويمتلك إشارةً وأعداد coefficient وأسًّا. وللحفاظ على الأعداد المعنوية لا تحذف اللغة الأصفار الأخيرة في الرقم. تضمّ الأعداد العشرية decimals كذلك قيمًا خاصّة مثل Infinity، و ‎-Infinity و NaN، وتميّز أيضًا بين القيمتين ‎-0 و ‎+0.

يمثّل سياق العمليات الحسابية بيئة تحدّد الدقة، وقواعد التقريب، وحدود الأسس (exponents)، ورايات (flags) تحدّد نتائج العمليات، ومفعّلات المصائد (trap enablers) تحدّد ما إذا كانت الإشارة تعامل معاملة الاستثناءات (exceptions). تقدّم اللغة خيارات التقريب التالية: ROUND_CEILING و ROUND_DOWN و ROUND_FLOOR و ROUND_HALF_DOWN و ROUND_HALF_EVEN و ROUND_HALF_UP و ROUND_UP و ROUND_05UP.

الإشارات هي مجموعة من الشروط الاستثنائية التي تظهر أثناء إجراء العمليات الحسابية. ويمكن تجاهل هذه الإشارات، أو عدّها مصدرًا للمعلومات، أو معاملتها كاستثناءات وذلك حسب الحاجة. تقدّم اللغة الإشارات التالية في وحدة decimal: ‏Underflow و Overflow و Subnormal و Rounded و Inexact و DivisionByZero و InvalidOperation و Clamped و FloatOperation.

لكل راية هناك trap enabler، وعندما ظهور إشارة يتم تعيين القيمة 1 للراية الخاصة بها، وبعد ذلك إن تم تعيين القيمة 1 لـ trap enabler تطلق اللغة استثناءً. تبقى قيمة الرايات ثابتة ويجب على المستخدم إعادة تعيين قيمتها قبل مراقبة الحسابات مرة أخرى.

راجع أيضًا: مواصفات IBM العامة للعمليات الحسابية على الأعداد العشرية، المواصفة العامة للعمليات الحسابية على الأعداد العشرية.

استخدام وحدة decimal

لاستخدام وحدة decimal يجب في البداية استيرادها، واستعراض السياق الحالي باستخدام الدالة getcontext()‎ وتعيين قيم جديدة لمقدار الدقة والتقريب والمصائد المفعّلة (enabled traps)، مثال:

>>> from decimal import *
>>> getcontext()
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
capitals=1, clamp=0, flags=[], traps=[Overflow, DivisionByZero,
InvalidOperation])
>>> getcontext().prec = 7 # تعيين مقدار جديد للدقة

يمكن بناء نسخ الكائن decimal من الأعداد الصحيحة، أو السلاسل النصية، أو الأعداد العشرية floats أو الصفوف tuples. عند استخدام الأعداد الصحيحة أو العشرية لبناء نسخة الكائن تجري عملية تحويل القيمة نفسها إلى النوع decimal. تتضمن الأعداد العشرية decimal قيمًا خاصّة مثل NaN وهي اختصار "Not a number"، وقيمة ما لانهاية الموجبة والسالبة، و ‎-0.

أمثلة

أمثلة نموذجية عن استخدام الوحدة Decimal لإجراء العمليات الأساسية على الأعداد العشرية:

>>> getcontext().prec = 28
>>> Decimal(10)
Decimal('10')
>>> Decimal('3.14')
Decimal('3.14')
>>> Decimal(3.14)
Decimal('3.140000000000000124344978758017532527446746826171875')
>>> Decimal((0, (3, 1, 4), -2))
Decimal('3.14')
>>> Decimal(str(2.0 ** 0.5))
Decimal('1.4142135623730951')
>>> Decimal(2) ** Decimal('0.5')
Decimal('1.414213562373095048801688724')
>>> Decimal('NaN')
Decimal('NaN')
>>> Decimal('-Infinity')
Decimal('-Infinity')

في حال التقاط إشارة FloatOperation فسيؤدي المزج بين نوعي الأعداد العشرية decimals و floats في بناء نسخة الكائن أو في معاملات المقارنة إلى إطلاق استثناء:

>>> c = getcontext()
>>> c.traps[FloatOperation] = True
>>> Decimal(3.14)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
>>> Decimal('3.5') < 3.7
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
>>> Decimal('3.5') == 3.5
True

الجديد في الإصدار 3.3

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

>>> getcontext().prec = 6
>>> Decimal('3.0')
Decimal('3.0')
>>> Decimal('3.1415926535')
Decimal('3.1415926535')
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal('5.85987')
>>> getcontext().rounding = ROUND_UP
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal('5.85988')

تطلق اللغة الاستثناء InvalidOperation عند تجاوز الحد المسموح به لدى إنشاء العدد العشري:

>>> Decimal("1e9999999999999999999")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]

التغييرات في الإصدار 3.3

تتفاعل الأعداد العشرية decimals بصورة جيدة مع الأنواع الأخرى للبيانات في بايثون. إليك بعض الأمثلة على ذلك:

>>> data = list(map(Decimal, '1.34 1.87 3.45 2.35 1.00 0.03 9.25'.split()))
>>> max(data)
Decimal('9.25')
>>> min(data)
Decimal('0.03')
>>> sorted(data)
[Decimal('0.03'), Decimal('1.00'), Decimal('1.34'), Decimal('1.87'),
Decimal('2.35'), Decimal('3.45'), Decimal('9.25')]
>>> sum(data)
Decimal('19.29')
>>> a,b,c = data[:3]
>>> str(a)
'1.34'
>>> float(a)
1.34
>>> round(a, 1)
Decimal('1.3')
>>> int(a)
1
>>> a * 5
Decimal('6.70')
>>> a * b
Decimal('2.5058')
>>> c % a
Decimal('0.77')

يمكن أيضًا استخدام بعض الدوال الرياضية مع الأعداد العشرية:

>>> getcontext().prec = 28
>>> Decimal(2).sqrt()
Decimal('1.414213562373095048801688724')
>>> Decimal(1).exp()
Decimal('2.718281828459045235360287471')
>>> Decimal('10').ln()
Decimal('2.302585092994045684017991455')
>>> Decimal('10').log10()
Decimal('1')

تقرّب دالة quantize()‎ العدد إلى أسّ ثابت، وهي طريقة مفيدة في التطبيقات المحاسبية التي غالبًا ما تقرّب النتائج إلى عدد ثابت من المراتب:

>>> Decimal('7.325').quantize(Decimal('.01'), rounding=ROUND_DOWN)
Decimal('7.32')
>>> Decimal('7.325').quantize(Decimal('1.'), rounding=ROUND_UP)
Decimal('8')

تتواصل الدالة getcontenxt()‎ مع السياق الحالي - كما هو مبين في أعلاه - وتتيح تعديل الخيارات حسب الحاجة، وهذه الطريقة ملائمة لمعظم التطبيقات. يمكن أيضًا إنشاء سياقات بديلة عند الحاجة إلى ذلك باستخدام المشيّد Context()‎، وتستخدم الدالة setcontext()‎ لتفعيل السياق البديل. وبحسب المعايير التي تعتمدها اللغة، تقدّم وحدة decimal سياقين قياسيين جاهزين للاستخدام هما BasicContext و ExtendedContext. السياق الأوّل مفيد جدًّا في عملية التنقيح debugging لأنّ معظم المصائد تكون مفعّلة:

>>> myothercontext = Context(prec=60, rounding=ROUND_HALF_DOWN)

>>> setcontext(myothercontext)

>>> Decimal(1) / Decimal(7)
Decimal('0.142857142857142857142857142857142857142857142857142857142857')
>>> ExtendedContext
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
capitals=1, clamp=0, flags=[], traps=[])
>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(7)
Decimal('0.142857143')
>>> Decimal(42) / Decimal(0)
Decimal('Infinity')
>>> setcontext(BasicContext)
>>> Decimal(42) / Decimal(0)
Traceback (most recent call last):
File "<pyshell#143>", line 1, in -toplevel-
Decimal(42) / Decimal(0)
DivisionByZero: x / 0

تمتلك السياقات كذلك رايات إشارة signal flags لمراقبة الحالات الاستثنائية التي تطرأ في أثناء إجراء الحسابات. تبقى الأعلام مفعّلة إلى حين إلغائها يدويًا؛ لذا يفضّل إلغاء الأعلام قبل أي مجموعة من العمليات الحسابية التي تخضع للمراقبة، وذلك باستخدام التابع clear_flags()‎:

>>> setcontext(ExtendedContext)
>>> getcontext().clear_flags()
>>> Decimal(355) / Decimal(113)
Decimal('3.14159292')
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[])

يبيّن مدخل flags أنّ القيمة التقريبية للعدد pi مقرّبة (تخلّصت اللغة من الأرقام التي تتجاوز مقدار دقة السياق) وأن النتيجة غير دقيقة (أي أنّ بعض الأعداد التي تم تجاهلها ليست أصفارًا). يمكن تعيين مصائد مفردة باستخدام القاموس في حقل traps الخاص بالسياق:

>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(0)
Decimal('Infinity')
>>> getcontext().traps[DivisionByZero] = 1
>>> Decimal(1) / Decimal(0)
Traceback (most recent call last):
File "<pyshell#112>", line 1, in -toplevel-
Decimal(1) / Decimal(0)
DivisionByZero: x / 0

تعدّل معظم البرامج السياق الحالي مرة واحدة فقط في بداية البرنامج، وتجري عملية تحويل البيانات إلى أعداد عشرية decimal في معظم البرامج دفعة واحدة باستخدام حلقة تكرارية. وبعد تعيين السياق وإنشاء الأعداد العشرية تعالج اللغة البيانات بنفس الطريقة التي تعالج بها الأنواع الرقمية الأخرى.

انظر أيضًا

مصادر