ننشئ التوابع السحرية العددية والمعكوسة كما رأينا سابقًا كائنات جديدة بدلًا من تعديل الكائنات الموضعية، إلا أن التوابع السحرية الموضعية المُستدعاة باستخدام معاملات الإسناد المدعوم مثل =+
و =*
تعدل الكائنات موضعيًا بدلًا من إنشاء كائنات جديدة (هناك استثناء سنشرحه في نهاية الفقرة). تبدأ أسماء هذه التوابع السحرية بحرف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
.
التابع السحري | المعامل | معامل المقارنة |
الدالة في وحدة operator
|
---|---|---|---|
()__eq__
|
يساوي |
==
|
operator.eq()
|
()__ne__
|
لا يساوي |
=!
|
operator.nt()
|
()__lt__
|
أصغر من |
<
|
operator.lt()
|
()__le__
|
أصغر أو يساوي |
=>
|
operator.le()
|
()__gt__
|
أكبر من |
<
|
operator.gt()
|
()__ge__
|
أكبر أو يساوي |
=<
|
operator.ge()
|
الجدول 3: توابع المقارنة السحرية ودوال وحدة operator
.
يمكنك رؤية تطبيق هذه التوابع في 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.