البرمجة كائنية التوجه في بايثون: التوابع السحرية الموضعية In-place Dunder Methods – بايثون

5

[ad_1]

ننشئ التوابع السحرية العددية والمعكوسة كما رأينا سابقًا كائنات جديدة بدلًا من تعديل الكائنات الموضعية، إلا أن التوابع السحرية الموضعية المُستدعاة باستخدام معاملات الإسناد المدعوم مثل =+ و =* تعدل الكائنات موضعيًا بدلًا من إنشاء كائنات جديدة (هناك استثناء سنشرحه في نهاية الفقرة). تبدأ أسماء هذه التوابع السحرية بحرفi، مثل ()__iadd__ و ()__imul__ من أجل العوامل =+ و =* على التتالي.

مثلًا، عندما تنفذ بايثون الشيفرة purse *= 2 لا يكون السلوك المتوقع أن تابع ()__imul__ الخاص بالصنف WizCoin سينشئ ويعيد كائن WizCoin جديد بضعف عدد النقود ويسنده للمتغير purse، ولكن بدلًا من ذلك، يعدل التابع ()__imul__ كائن WizCoin الحالي في purse ليكون له ضعف عدد النقود. هذا فرق بسيط ولكن مهم إذا أردت لأصنافك أن تقوم بتحميل زائد overload لمعاملات الإسناد المدعومة.

عرّف الصنف ‘WizCoin’ الذي أنشأناه العاملين + و *، لذا لنُعرّف التابعين السحريين ()__iadd__ و ()__imul__ ليتمكّنوا بدورهم من تعريف العاملين =+ و =* أيضًا، نستدعي في التعبيرين purse += tipJar و purse *= 2 التابعين ()__iadd__ و ()__imul__ على التتالي وتمرر tipJar و 2 إلى المعامل other على التتالي.

ضِف التالي إلى نهاية ملف wizcoin.py:

--snip--
    def __iadd__(self, other):
        """Add the amounts in another WizCoin object to this object."""
        if not isinstance(other, WizCoin):
            return NotImplemented

        # نعدل من قيمة الكائن‫ self موضعيًا
        self.galleons += other.galleons
        self.sickles += other.sickles
        self.knuts += other.knuts
        return self  # تعيد التوابع السحرية الموضعية القيمة‫ self على الدوام تقريبًا

    def __imul__(self, other):
        """Multiply the amount of galleons, sickles, and knuts in this object
        by a non-negative integer amount."""
        if not isinstance(other, int):
            return NotImplemented
        if other < 0:
            raise WizCoinException('cannot multiply with negative integers')

        # يُنشئ الصنف‫ WizCoin كائنات متغيّرة، لذا لا تنشئ كائن جديد كما هو موضح في الشيفرة المعلّقة:
        #return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other)

        # نعدل من قيمة الكائن‫ self موضعيًا
        self.galleons *= other
        self.sickles *= other
        self.knuts *= other
        return self  #  تعيد التوابع السحرية الموضعية القيمة‫ self دائمًا تقريبًا

يمكن أن تستخدم كائنات WizCoin العامل =+ مع كائنات WizCoin أخرى والعامل ‎=* مع الأعداد الصحيحة الموجبة. تعدّل التوابع الموضعية الكائن ‘self’ موضعيًا بدلًا من إنشاء كائن ‘WizCoin’ جديد بعد التأكد من أن المعامل الآخر صالح. أدخل التالي إلى الصدفة التفاعلية لرؤية كيف تعدل عوامل الإسناد المدعوم كائنات WizCoin موضعيًا:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)
>>> tipJar = wizcoin.WizCoin(0, 0, 37)
1 >>> purse + tipJar
2 WizCoin(2, 5, 46)
>>> purse
WizCoin(2, 5, 10)
3 >>> purse += tipJar
>>> purse
WizCoin(2, 5, 47)
4 >>> purse *= 10
>>> purse
WizCoin(20, 50, 470)

يستدعي العامل + التابعين السحريين ()__add__ و ()__radd__ لإنشاء وإعادة كائنات جديدة. تبقى الكائنات الأصلية التي يعمل عليها العامل + على حالها. يجب على التوابع السحرية الموضعية أن تعدل الكائنات موضعيًا طالما أن الكائن متغيّر mutable (أي هو كائن يمكن تغيير قيمته). الاستثناء هو للكائنات الثابتة immutable objects، إذ لا يمكن تعديلها ومن المستحيل تعديلها موضعيًا. في هذه الحالة يجب على التابع السحري الموضعي إنشاء وإعادة كائن جديد كما في التوابع السحرية العددية والمعكوسة.

إذا لم نجعل السمات galleons و sickles و knuts للقراءة فقط، فهذا يعني أنه يمكن تعديلها، وبالتالي كائنات WizCoin هي متغيّرة، كما أن معظم الأصناف التي تكتبه تُنشئ كائنات متغيّرة لذا يجب تصميم توابع سحرية موضعية لتعديل الكائن موضعيًا.

تستدعي بايثون تلقائيًا التابع السحري العددي في حال لم تُنفذ التابع السحري الموضعي. مثلًا، إذا لم يكن للصنف WizCoin تابع ()__imul__ سيستدعي التعبير purse *= 10 التابع ()__mul__ بدلًا عنه ويسند له القيمة المرجعة purse، لأن كائنات WizCoin متغيّرة وهذا سلوك غير متوقع وقد يؤدي لأخطاء بسيطة.

توابع المقارنة السحرية

يحتوي تابع ‎‎sort‎‎‎‎‎()‎‎‎ ودالة sorted()‎‎‏‏ خوارزميات ترتيب فعالة، ويمكن الوصول إليها باستدعاء بسيط، ولكن إذا أردت ترتيب ومقارنة كائنات أصنافك، ستحتاج لإخبار بايثون كيفية المقارنة بين الكائنين عن طريق تنفيذ توابع المقارنة السحرية، تستدعي بايثون التوابع المقارنة في الخلفية عندما تُستخدم الكائنات الخاصة بك في التعبير مع عوامل المقارنة< و > و =< و => و == و =!.

قبل أن نستكشف توابع المقارنة السحرية، فلنفحص الدوال الست في وحدة ‘operator’ التي تنجز نفس وظائف عوامل المُقارنة الستة، إذ ستستدعي توابع المقارنة السحرية هذه الدوال. أدخل التالي في الصدفة التفاعلية:

>>> import operator
>>> operator.eq(42, 42)        # أي يساوي، وهي مماثلة للتعبير 42 == 42
True
>>> operator.ne('cat', 'dog')  # أي لا يساوي وهي مماثلة للتعبير‫ 'cat' != 'dog'
True
>>> operator.gt(10, 20)        # أكبر من، وهي مماثلة للتعبير 20 < 10
False
>>> operator.ge(10, 10)        # أكبر من أو يساوي، وهي مماثلة للتعبير 10 =< 10
True
>>> operator.lt(10, 20)        # أصغر من، وهي مماثلة للتعبير 20 > 10
True
>>> operator.le(10, 20)        # أصغر من أو يساوي وهي مماثلة للتعبير 10 => 20
True

ستعطينا وحدة operator نسخ دوال من عوامل المقارنة ويكون تنفيذها بسيط. مثلًا يمكننا كتابة دالة operator.eq()‎ في سطرين:

def eq(a, b):
    return a == b

من المفيد امتلاك نسخ لعوامل المقارنة على هيئة دوال لأنه على عكس العوامل، يمكن تمرير الدوال مثل وسطاء لاستدعاءات الدالة، وسنفعل ذلك لتنفيذ تابع مساعدة لتوابع المقارنة السحرية.

أولًا، ضِف التالي إلى بداية wizcoin.py، إذ تعطي تعليمات الاستيراد import هذه الإذن بالوصول للدوال في وحدة operator وتسمح لك بالتحقق أن الوسيط other في التابع هو متتالية sequence عن طريق مقارنته مع collections.abc.Sequence:

import collections.abc
import operator

ثم ضِف التالي في نهاية ملف wizcoin.py:

--snip--
1     def _comparisonOperatorHelper(self, operatorFunc, other):
        """A helper method for our comparison dunder methods."""

2         if isinstance(other, WizCoin):
            return operatorFunc(self.total, other.total)
3         elif isinstance(other, (int, float)):
            return operatorFunc(self.total, other)
4         elif isinstance(other, collections.abc.Sequence):
            otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2]
            return operatorFunc(self.total, otherValue)
        elif operatorFunc == operator.eq:
            return False
        elif operatorFunc == operator.ne:
            return True
        else:
            return NotImplemented

    def __eq__(self, other):  # eq is "EQual"
5         return self._comparisonOperatorHelper(operator.eq, other)

    def __ne__(self, other):  # ne is "Not Equal"
6         return self._comparisonOperatorHelper(operator.ne, other)

    def __lt__(self, other):  # lt is "Less Than"
7         return self._comparisonOperatorHelper(operator.lt, other)

    def __le__(self, other):  # le is "Less than or Equal"
8         return self._comparisonOperatorHelper(operator.le, other)

    def __gt__(self, other):  # gt is "Greater Than"
9        return self._comparisonOperatorHelper(operator.gt, other)

    def __ge__(self, other):  # ge is "Greater than or Equal"
a        return self._comparisonOperatorHelper(operator.ge, other)

تستدعي توابع المقارنة السحرية التابع ‎__comparisonOperatorHelper()‎ وتمرر الدالة المناسبة من وحدة operator إلى المعامل operatorFunc، عند استدعاء operatorFunc()‎ فنحن هنا نستدعي الدالة المُمرّرة إلى معامل operatorFunc الذي هو eq()‎ أو ne()‎ أو lt()‎ أو le()‎ أو gt()‎ أو ge()‎ من وحدة operator، أو سيكون علينا تكرار الشيفرة في ‎__comparisonOperatorHelper()‎ في كل من توابع المقارنة السحرية الستة.

ملاحظة: تدعى الدوال (أو التوابع) التي تقبل دوال أخرى على أنها وسطاء، مثل ‎__comparisonOperatorHelper()‎ بدوال المراتب الأعلى higher-order functions.

يمكن الآن مقارنة كائنات WizCoin مع كائنات WizCoin أخرى وأعداد صحيحة وعشرية وقيم سلسلة من ثلاث قيم عددية تمثل galleons و sickles و knuts. أدخل التالي في الصدفة التفاعلية لرؤية الأمر عمليًا:

>>> import wizcoin
>>> purse = wizcoin.WizCoin(2, 5, 10)  # إنشاء كائن‫ WizCoin
>>> tipJar = wizcoin.WizCoin(0, 0, 37) # إنشاء كائن‫ WizCoin آخر
>>> purse.total, tipJar.total # فحص القيم وفقًا إلى‫ knuts
(1141, 37)
>>> purse > tipJar # ‫المقارنة بين كائنات WizCoin باستخدام عامل مقارنة
True
>>> purse < tipJar
False
>>> purse > 1000 # الموازنة مع عدد صحيح
True
>>> purse <= 1000
False
>>> purse == 1141
True
>>> purse == 1141.0 # المقارنة مع عدد عشري
True
>>> purse == '1141' # ‫كائن WizCoin ليس مساويًا لأي قيمة سلسلة نصية
False
>>> bagOfKnuts = wizcoin.WizCoin(0, 0, 1141)
>>> purse == bagOfKnuts
True
>>> purse == (2, 5, 10) # يمكننا المقارنة مع صف يتكون من ثلاثة أعداد صحيحة
True
>>> purse >= [2, 5, 10] # يمكننا المقارنة مع قائمة تحتوي على ثلاثة أعداد صحيحة
True
>>> purse >= ['cat', 'dog'] # يجب أن تتسبب هذه التعليمة بخطأ
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\Al\Desktop\wizcoin.py", line 265, in __ge__
    return self._comparisonOperatorHelper(operator.ge, other)
  File "C:\Users\Al\Desktop\wizcoin.py", line 237, in _comparisonOperatorHelper
    otherValue = (other[0] * 17 * 29) + (other[1] * 29) + other[2]
IndexError: list index out of range

يستدعي التابع المساعد isinstance(other, collections.abc.Sequence)‎ لرؤية ما إذا كان other هو نوع بيانات متتالية مثل صف tuple أو قائمة list. بإمكاننا كتابة شيفرة مثل purse >= [2, 5, 10]‎ لعمل مقارنة سريعة، وذلك بجعل كائنات WizCoin قابلة للمقارنة مع متتاليات.

مقارنة المتتاليات

تضع بايثون أهمية أكبر على العناصر الأولى في المتتالية عند مقارنة كائنين من أنواع المتتاليات المضمنة مثل السلاسل النصية والقوائم والصفوف أي أنها لا تقارن العناصر الأخيرة إلا إذا كانت لدى العناصر الأولى قيم متساوية. مثلًا أدخل التالي في الصدفة التفاعلية:

>>> 'Azriel' < 'Zelda'
True
>>> (1, 2, 3) > (0, 8888, 9999)
True

تأتي السلسلة النصية Azriel قبل (أي هي أقل من) Zelda لأن 'A' تأتي قبل 'Z'. الصف (3, 2, 1) يأتي بعد (أي هو أكبر من) (9999, 8888, 0) لأن 1 هي أكبر من 0. أدخل التالي في الصدفة التفاعلية:

>>> 'Azriel' < 'Aaron'
False
>>> (1, 0, 0) > (1, 0, 9999)
False

لا تأتي Azriel قبل Aaron على الرغم من أن 'A' في 'Azriel' تساوي 'A' في 'Aaron' ولكن 'z' التالية في 'Azriel' لا تأتي قبل 'a' في 'Aaron'، ويمكن تطبيق الشيء ذاته في الصفين (1, 0, 0) و (1, 0, 9999)، إذ أن العنصرين في كل صف متساويين لذا تحدد العناصر الثالثة (0 و 9999 على التتالي) أن (0, 0, 1) تأتي قبل (9999, 0, 1).

هذا يجبرنا على اتخاذ قرار بشأن تصميم صنف WizCoin فهل يجب أن تأتي WizCoin(0, 0, 9999)‎ قبل أو بعد WizCoin(1, 0, 0)‎؟ إذا كان عدد galleons أهم من عدد sickles أو knuts فيجب على WizCoin(0, 0, 9999)‎ أن تأتي قبل WizCoin(1, 0, 0)‎، أما إذا قارننا الكائنات بالاعتماد على قيمة knuts فيجب أن تأتي WizCoin(0, 0, 9999)‎ (قيمتها ‎9999 knuts) بعد WizCoin(1, 0, 0)‎ (قيمتها 493‎ knuts).وُضعت قيمة الكائن في ملف wzicoin.py على أنها مقدرة بـ knuts لأنها تجعل السلوك متناسقًا مع كيفية مقارنةWizCoin مع الأعداد الصحيحة والعشرية. هذا نوع من الاختيارات التي يجب أن تفعلها عند تصميم الأصناف الخاصة بك.

لا توجد توابع سحرية مقارنة معكوسة مثل ()__req__ أو ()__rne__ تحتاج لتنفيذها، وبدلًا عن ذلك نجد أن ()__lt__ و ()__gt__ تعكس بعضها و ()__le__ و ()__ge__ تعكس بعضها و ()__eq__ و ()__ne__ تعكس نفسها، سبب ذلك هو أن العلاقات التالية صحيحة مهما كانت القيم في يمين أو يسار المعامل.

  • purse > [2, 5, 10]‎ هي نفس ‎[2, 5, 10] < purse
  • purse >= [2, 5, 10]‎ هي نفس ‎[2, 5, 10] <= purse
  • purse == [2, 5, 10]‎ هي نفس ‎[2, 5, 10] == purse
  • purse! = [2, 5, 10]‎ هي نفس ‎[2, 5, 10] != purse

بمجرد تطبيقك للدوال السحرية المقارنة، ستستخدم بايثون تلقائيًا دالة sort()‎ لترتيب الكائنات الخاصة بك. أدخل التالي في الصدفة التفاعلية:

>>> import wizcoin
>>> oneGalleon = wizcoin.WizCoin(1, 0, 0) #  ‫تكافئ 493 knut
>>> oneSickle = wizcoin.WizCoin(0, 1, 0)  #  ‫تكافئ 29 knut
>>> oneKnut = wizcoin.WizCoin(0, 0, 1)    # ‫تكافئ 1 knut
>>> coins = [oneSickle, oneKnut, oneGalleon, 100]
>>> coins.sort() # رتّب من القيمة الأقل إلى الأعلى
>>> coins
[WizCoin(0, 0, 1), WizCoin(0, 1, 0), 100, WizCoin(1, 0, 0)] 

يحتوي الجدول 3 قائمة كاملة من توابع المقارنة السحرية ودوال operator.

[الجدول 3: توابع المقارنة السحرية ودوال وحدة operator.]

التابع السحري المعامل معامل المقارنة الدالة في وحدة operator
()__eq__ يساوي == operator.eq()‎
()__ne__ لا يساوي =! operator.nt()‎
()__lt__ أصغر من < operator.lt()‎
()__le__ أصغر أو يساوي => operator.le()‎
()__gt__ أكبر من < operator.gt()‎
()__ge__ أكبر أو يساوي =< operator.ge()‎

يمكنك رؤية تطبيق هذه التوابع في https://autbor.com/wizcoinfull.

التوثيق الكامل لتوابع المقارنة السحرية في توثيقات بايثون https://docs.python.org/3/reference/datamodel.html#object.lt.

الخلاصة

تسمح توابع المقارنة السحرية لكائنات الأصناف الخاصة بك أن تستخدم معاملات بايثون للمقارنة بدلًا من إجبارك على إنشاء توابع خاصة بك. إذا كنت تُنشئ توابعًا اسمها equals()‎ و isGreaterThan()‎ فهذه ليست خاصة ببايثون، وعدّ هذه إشارة لك لتبدأ باستخدام توابع المقارنة السحرية.

ترجمة -وبتصرف- لقسم من الفصل PYTHONIC OOP: PROPERTIES AND DUNDER METHODS من كتاب Beyond the Basic Stuff with Python.

اقرأ المزيد

[ad_2]

المصدر