البحث في الموقع
المحتوى عن 'بعد أساسيات بايثون'.
-
لدى لغة بايثون Python أسماء توابع خاصة تبدأ وتنتهي بشرطتين سفليتين وتختصر بالسحرية، وتسمى عادةً التوابع السحرية أو التوابع الخاصة أو توابع داندر Dunder Methods، أنت تعرف مسبقًا اسم التابع السحري __init__() ولكن لدى بايثون العديد غيره، نستخدمهم عادةً لزيادة تحميل المعامل، أي إضافة سلوكيات خاصة تسمح لنا باستخدام كائنات الأصناف الخاصة بنا مع معاملات بايثون، مثل + أو >=. تسمح التوابع السحرية الأخرى لكائنات الأصناف الخاصة بنا بالعمل مع وظائف بايثون المضمنة مثل len() و repe(). كما هي الحال في __init__() أو توابع الجلب والضبط والحذف، لا نستدعي التوابع السحرية مباشرةً، يل تستدعيهم بايثون في الخلفية عندما تستخدم الكائنات مع المعاملات أو بعض الوظائف المضمنة. مثلًا، إذا أنشأت تابعًا اسمه __len__() أو __repr__() للأصناف الخاصة بك فستُستدعى في الخلفية عندما يمرر كائن من هذا الصنف إلى الدالة len() أو repr() على التوالي. هذه التوابع موثقة على الويب في توثيقات بايثون الرسمية. سنحرص على التوسع في صنف WizCoin أثناء استكشافنا لأنواع التوابع السحرية المختلفة وذلك لتحقيق أكبر استفادة ممكنة. توابع تمثيل السلاسل النصية السحرية يمكن استخدام التوابع السحرية __repr__() و __str__() لإنشاء سلسلة نصية تمثل كائنات لا تتعامل معها بايثون عادةً، إذ تُنشئ بايثون عادةً سلاسل تمثيل نصية للكائنات بطريقتين، سلسلة repr النصية وهي سلسلة نصية لشيفرة بايثون التي تُنشئ نسخة من الكائن عندما تُنفذ، وسلسلة str النصية التي هي سلسلة يستطيع الإنسان قراءتها وتؤمن معلومات واضحة ومفيدة عن الكائن. تعاد سلاسل repr و str عن طريق الدوال المبنية مسبقًا repr() و str() على التوالي. مثلًا، أدخل التالي إلى الصدفة التفاعلية لرؤية السلسلتين النصيتين repr و str للكائن datetime.date: >>> import datetime 1 >>> newyears = datetime.date(2021, 1, 1) >>> repr(newyears) 2 'datetime.date(2021, 1, 1)' >>> str(newyears) 3 '2021-01-01' 4 >>> newyears datetime.date(2021, 1, 1) في هذا المثال، سلسلة repr -أي datetime.date(2021, 1, 1)- للكائن datetime.date(السطر 2) هي حرفيًا سلسلة نصية لشيفرة بايثون التي تُنشئ نسخةً من الكائن (السطر 1). تؤمن هذه النسخة تمثيلًا دقيقًا للكائن، ومن جهة أخرى، السلسلة النصية str -أي 2021-01-01- للكائن datetime.date (السطر 3) هي سلسلة نصية تمثل قيمة الكائن بطريقة سهلة القراءة للبشر. إذا أدخلنا ببساطة الكائن في الصدفة التفاعلية (السطر 4)، تظهِر السلسلة النصية repr. تظهر غالبًا السلسلة النصية str للمستخدمين وتُستخدم السلسلة النصية repr للكائن في السياق التقني مثل رسائل الخطأ والسجلات. تعلم بايثون كيفية إظهار الكائنات في أنواعها المبنية مسبقًا مثل الأعداد الصحيحة والسلاسل النصية، ولكنها لا تعلم كيفية إظهار الكائنات للأصناف التي أنشأناها نحن. إذا لم يعرف repr() كيفية إنشاء سلسلة نصية repr أو str لكائن، ستكون السلسلة النصية مغلفة بأقواس مثلثة وتحتوي عنوان الذاكرة واسم للكائن '<wizcoin.WizCoin object at 0x00000212B4148EE0>' لإنشاء هذا النوع من السلاسل النصية لكائن WizCoin أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> str(purse) '<wizcoin.WizCoin object at 0x00000212B4148EE0>' >>> repr(purse) '<wizcoin.WizCoin object at 0x00000212B4148EE0>' >>> purse <wizcoin.WizCoin object at 0x00000212B4148EE0> لا تمتلك هذه السلاسل فائدة كبيرة وصعبة القراءة، لذا يمكننا إخبار بايثون ما نريد استخدامه عن طريق تطبيق التوابع السحرية __repr__() و __str__()؛ إذ يحدد التابع __repr__() أي سلسلة نصية يجب أن تُعيدها بايثون عندما يمرر الكائن إلى الدالة المبنية مسبقًا repr()؛ بينما يحدد التابع __str__() أي سلسلة نصية يجب أن تُعيدها بايثون عندما يمرر الكائن إلى الدالة المبنية مسبقًا str(). ضِف التالي إلى نهاية ملف wizcoin.py: --snip-- def __repr__(self): """Returns a string of an expression that re-creates this object.""" return f'{self.__class__.__qualname__}({self.galleons}, {self.sickles}, {self.knuts})' def __str__(self): """Returns a human-readable string representation of this object.""" return f'{self.galleons}g, {self.sickles}s, {self.knuts}k' عندما نمرر purse إلى repr() و str() يستدعي بايثون التوابع السحرية __repr__() و __str__()، أي نحن لا نستدعي التوابع السحرية في الشيفرة الخاصة بنا. لاحظ أن السلسة النصية f التي تضم الكائن في الأقواس تستدعي ضمنًا str() للحصول على السلسة النصية str. مثلًا أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> repr(purse) # Calls WizCoin's __repr__() behind the scenes. 'WizCoin(2, 5, 10)' >>> str(purse) # Calls WizCoin's __str__() behind the scenes. '2g, 5s, 10k' >>> print(f'My purse contains {purse}.') # Calls WizCoin's __str__(). My purse contains 2g, 5s, 10k. عندما نمرر الكائن WizCoin في purse إلى الدالتين repr() و str()، تستدعي بايثون في الخلفية التابعين __repr__() و __str__() الخاصين بالصنف WizCoin. برمجنا هذين التابعين ليعيدا سلاسلًا نصيةً مفيدةً وسهلة القراءة. إذا أدخلت نص السلسلة النصية repr التالية 'WizCoin(2, 5, 10)' إلى الصدفة التفاعلية ستُنشئ كائن WizCoin لديه نفس سمات الكائن في purse. السلسلة النصية str هي تمثيل أسهل للقراءة لقيمة الكائن 2g, 5s, 10k. إذا استخدمت الكائن WizCoin في السلسلة النصية f، ستستخدم بايثون السلسلة النصية str الخاصة بالكائن. إذا كانت الكائنات WizCoin معقدة لدرجة أنه من المستحيل إنشاء نسخة منها باستدعاء دالة بانية Constructor Function واحدة، نغلف السلسلة النصية repr في قوسين مثلثين للتنويه على أنه لا يمكن أن تصبح شيفرة بايثون. هكذا تكون سلسلة تمثيل نصي العامة، مثل '<wizcoin.WizCoin object at 0x00000212B4148EE0>'. كتابة ذلك في الصَدَفة التفاعلية سيرفع خطأ SyntaxError حتى لا يحدث ارتباك بشيفرة بايثون التي تُنشئ نسخة من ذلك الكائن. نستخدم __self.__class__.__qualname بدلًا من توفير السلسلة النصية WizCoin في الشيفرة داخل التابع __repr__()، إذ يستخدم التابع الموروث __repr__() اسم الصنف الفرعي بدلًا من WizCoin. إذا أعدنا تسمية الصنف WizCoin سيستخدم التابع __repr__() الاسم الجديد تلقائيًا. تظهِر السلسلة النصية str للكائن WizCoin السمة بصورة أنيقة ومختصرة. يُفضّل جدًا تطبيق __repr__() و __str__() في كل الأصناف الخاصة بك. المعلومات الحساسة في سلاسل REPR النصية كما ذكرنا سابقًا، نظهر السلاسل النصية str للمستخدمين ونستعمل السلاسل النصية repr في سياق تقني مثل السجلات. ولكن يمكن أن تسبب السلاسل النصية repr مشاكل أمنية، إذا كان الكائن المُنشئ يحتوي على معلومات حساسة مثل كلمات المرور والتفاصيل الطبية والمعلومات الشخصية؛ ففي هذه الحالة تأكد من خلو التابع __repr__() من هذه المعلومات في السلسلة النصية المرجعة، وعند تعطل البرنامج، يجري إعداده بصورة متكررة لتضمين محتويات المتغيرات في ملف السجل للمساعدة في تصحيح الأخطاء، ولا تُعامل عادةً ملفات الدخول هذه على أنها معلومات حساسة. تحتوي ملفات الدخول المفتوحة للعلن في العديد من الحوادث الأمنية كلمات المرور وأرقام بطاقات بنكية وعناوين المنازل ومعلومات حساسة أخرى، خذ ذلك بالحسبان عند كتابة التوابع __repr__() الخاص بصنفك. التوابع السحرية العددية Numeric Dunder Methods تزيد التوابع السحرية العددية أو التوابع السحرية الرياضية من تحميل عامل بايثون الرياضية، مثل + و - و * و / وما شابه. لا نستطيع حاليًا تنفيذ عملية رياضية مثل جمع كائني WizCoin باستخدام العامل +، وإذا حاولنا فعل ذلك سترفع بايثون استثناء TypeError لأنها لا تعرف كيفية إضافة كائنات WizCoin. أدخل التالي إلى الصدفة التفاعلية لمشاهدة هذا الخطأ: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> tipJar = wizcoin.WizCoin(0, 0, 37) >>> purse + tipJar Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'WizCoin' and 'WizCoin' يمكنك استخدام التابع السحري ()__add__ بدلًا من كتابة التابع addWizCoin() لصنف WizCoin، لكي تعمل كائنات WizCoin مع العامل +. أضف التالي إلى نهاية ملف wizcoin.py: --snip-- 1 def __add__(self, other): """Adds the coin amounts in two WizCoin objects together.""" 2 if not isinstance(other, WizCoin): return NotImplemented 3 return WizCoin(other.galleons + self.galleons, other.sickles + self.sickles, other.knuts + self.knuts) تستدعي بايثون التابع ()__add__عندما يكون الكائن WizCoin على يسار المعامل + وتمرر القيمة على الجانب الأيمن من المعامل + للمعامل other (يمكن تسمية المعامل أي شيء ولكن الاصطلاح هو other). تذكر أنه يمكن تمرير أي نوع من أنواع الكائنات إلى التابع ()__add__، لذا يجب على التابع أن يحتوي اختبارات من النوع، فمثلًا ليس من المنطقي إضافة رقم عشري أو عدد صحيح إلى كائن WizCoin لأننا لا نعرف إذا كان يجب إضافته إلى galleons أو sickles أو knuts. يُنشئ التابع ()__add__ كائن WizCoin جديد مع كميات تساوي مجموع السمات galleons و sickles و knuts من self و other3 لأن هذه السمات الثلاث تحتوي الأعداد الصحيحة التي يمكننا استخدام المعامل + عليهم. الآن بعد أن حمّلنا العامل + لصنف WizCoin، يمكننا استخدام العامل + على الكائن WizCoin. يسمح لنا زيادة تحميل العامل + بكتابة شيفرة أكثر قابليّة للقراءة. مثلًا، أدخل التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن WizCoin >>> tipJar = wizcoin.WizCoin(0, 0, 37) # إنشاء كائن WizCoin آخر >>> purse + tipJar # إنشاء كائن WizCoin آخر يحتوي على المجموع WizCoin(2, 5, 47) إذا مُرر نوع الكائن الخطأ إلى other، لن يرفع التابع السحري استثناءً ولكنه سيعيد القيمة المبنية مسبقًا NotImplemented، فمثلًا، other في الشيفرة التالية هي عدد صحيح: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse + 42 # لا يمكن إضافة كائنات WizCoin مع الأعداد الصحيحة Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'WizCoin' and 'int' تشير إعادة NotImplemented إلى بايثون لاستدعاء التوابع لتؤدي هذه العملية. سنتوسع حول "التوابع السحرية العددية المعكوسة" بتفصيل أكثر في هذا المقال. تستدعي بايثون في الخلفية التابع ()__add__ مع 42 للمعامل other الذي يعيد NotImplemented مما يؤدي لأن ترفع بايثون TypeError. على الرغم من أنه لا يجب إضافة الأعداد الصحيحة أو طرحهم من الكائن WizCoin إلا أنه من المنطقي السماح للشيفرة بضرب كائنات WizCoin بأعداد صحيحة موجبة عن طريق تعريف تابع سحري ()__mul__. ضِف التالي في نهاية الملف wizcoin.py: --snip-- def __mul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" if not isinstance(other, int): return NotImplemented if other < 0: # Multiplying by a negative int results in negative # amounts of coins, which is invalid. raise WizCoinException('cannot multiply with negative integers') return WizCoin(self.galleons * other, self.sickles * other, self.knuts * other) يسمح لك التابع ()__mul__ بضرب كائنات WizCoin بأعداد صحيحة موجبة. إذا كان other عدد صحيح، فهذا يعني أنه نوع البيانات التي يتوقعه التابع ()__mul__ ولا يجب أن نعيد NotImplemented. ولكن إذا كان العدد الصحيح سالبًا، هذا يعني أن ضربه الكائن WizCoin سيعطي قيم سلبية للنقود في الكائن WizCoin لأن هذا يتعارض مع تصميمنا للصنف، نرفع WizCoinException مع رسالة خطأ مفصلة. ملاحظة: لا يجب تغيير الكائن self في التابع السحري العددي، بل يجب على التابع إنشاء وإعادة كائن جديد بدلًا من ذلك، إذ يُتوقع من العامل + ومن باقي العوامل أيضًا تقييم كائن جديد بدلًا من تعديل قيمة الكائن الموضعي. أدخل التالي في الصدفة التفاعلية لمشاهدة عمل التابع السحري ()__mul__: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) # إنشاء كائن WizCoin >>> purse * 10 # اضرب كائن WizCoin بعدد صحيح WizCoin(20, 50, 100) >>> purse * -2 # الضرب بعدد صحيح سالب يتسبب بخطأ Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\Desktop\wizcoin.py", line 86, in __mul__ raise WizCoinException('cannot multiply with negative integers') wizcoin.WizCoinException: cannot multiply with negative integers يظهر الجدول 1 قائمة التوابع السحرية العددية، لا توجد حاجة لتنفيذ كل التوابع في الصنف الخاص بك. حدد التوابع التي تفيدك. التابع السحري العملية المعامل أو الدالة المضمنة ()__add__ جمع + ()__sub__ طرح - ()__mul__ ضرب * ()__matmul__ ضرب المصفوفات (جديد في بايثون 3.5) @ ()__truediv__ قسمة / ()__floordiv__ قسمة عدد صحيح // ()__mod__ نسبة % ()__divmod__ قسمة ونسبة divmode() ()__pow__ رفع للأس **, pow ()__lshift__ انتقال لليسار >> ()__rshift__ انتقال لليمين << ()__and__ عملية ثنائية و & ()__or__ عملية ثنائية أو | ()__xor__ عملية ثنائية أو حصرية ^ ()__neg__ سلبي أحادي - كما في -42 ()__pos__ هوية أحادي + كما في +42 ()__abs__ قيمة مطلقة ()abs ()__invert__ عملية ثنائية عكس ~ ()__complex__ شكل العدد العقدي complex() ()__int__ شكل العدد الصحيح int() ()__float__ شكل العدد العشري float() ()__bool__ شكل بولياني bool() ()__round__ التدوير round() ()__trunc__ الاختصار math.trunc() ()__floor__ التدوير للأسفل math.floor() ()__ceil__ التدوير للأعلى math.ceil() الجدول 1: التوابع السحرية العددية بعض هذه التوابع مهمة لصنف WizCoin، حاول كتابة التطبيق الخاص بك لكل من التوابع ()__sub__ و ()__pow__ و ()__int__ و ()__float__ و ()__bool__. يمكنك مشاهدة أمثلة عن التطبيقات من خلال الرابط https://autbor.com/wizcoinfull. التوثيق الكامل للتوابع السحرية العددية موجود في توثيقات بايثون على الرابط https://docs.python.org/3/reference/datamodel.html#emulating-numeric-types. تسمح التوابع السحرية العددية للكائنات الخاصة بأصنافك استخدام العوامل الرياضية الخاصة ببايثون. استخدم التوابع السحرية العددية في حال كتبت توابع تصف مهمة تابع موجود سابقًا أو دالة مبنية مسبقًا، مثل التابعين multiplyBy() أو convertToInt() أو ما شابه، إضافةً إلى التوابع السحرية المعكوسة أو الموضعية المشروحة في الفقرتين التاليتين. التوابع السحرية العددية المعكوسة تستدعي بايثون التوابع السحرية العددية عندما يكون الكائن على يسار العامل الرياضي، ولكنها تستدعي التابع السحري العددي المعكوس (يسمى أيضًا التابع السحري العددي العكوس أو اليد اليمين) عندما يكون الكائن على الطرف اليمين من العامل الرياضي. التوابع السحرية العددية المعكوسة مفيدة لأن المبرمجين الذين يستخدمون الأصناف الخاصة بك لا يكتبون دومًا الكائن على الطرف اليسار من العامل الذي يقود بدوره لسلوك غير متوقع. لنرى مثلًا ما سيحدث عندما تحتوي purse كائن WizCoin وتعطي بايثون القيمة للتعبير 2 * purse حيث purse هي على الطرف اليمين للمعامل. يُستدعى التابع ()__mul__ للصنف int لأن 2 هو عدد صحيح، وذلك مع تمرير purse للمعامل other. لا يعرف التابع ()__mul__ للصنف int كيف يتعامل مع الكائنات WizCoin لذا يُعيد NotImplemented. لا ترفع بايثون الخطأ TypeError الآن لأن purse تحتوي كائن WizCoin، ويُستدعى التابع ()__rmul__ الخاص بالصنف WizCoin باستخدام 2 ويُمرر إلى المعامل other. ترفع بايثون الخطأ TypeError إذا أعاد التابع __rmul__() القيمة NotImplemented. ما عدا ذلك تكون القيمة المعادة من ()__rmul__ هي نتيجة التعبير 2 * purse. يعمل التعبيرpurse * 2 بصورة مختلفة عندما تكون purse على الجانب الأيسر من المعامل: لأن purse تحتوي كائن WizCoin، إذ يُستدعى تابع ()__mul__ الخاص بالصنف WizCoin ويمرر 2 للمعامل other. ينشئ التابع ()__mul__ كائن WizCoin جديد ويعيده. الكائن المُعاد هو قيمة التعبير purse * 2. لدى التوابع السحرية العددية والتوابع السحرية العددية المعكوسة نفس الشيفرة إذا كانت متبادلة. العوامل المتبادلة مثل الجمع لديها نفس النتيجة بالاتجاهين، 3+2 هي نفس 2+3، ولكن المعاملات الأخرى ليست تبادلية فمثلًا 3-2 ليست 2-3. أي عملية تبادلية يمكنها استدعاء نفس التابع السحري العددي الأساسي عندما يُستدعى التابع السحري العددي المعكوس؛ فمثلًا، أضف التالي في نهاية ملف wizcoin.py لتعريف التابع السحري العددي المعكوس لعامل الضرب: --snip-- def __rmul__(self, other): """Multiplies the coin amounts by a non-negative integer.""" return self.__mul__(other) ضرب عدد صحيح بكائن WizCoin هو تبادلي، إذ أن 2 * purse هي نفس purse * 2. بدلًا من نسخ ولصق الشيفرة من ()__mul__ نستدعي فقط self.__mul__() ونمررها للمعامل other. بعد تحديث mizcoin.py، جرب استخدام تابع الضرب السحري المعكوس عن طريق إدخال التالي إلى الصدفة التفاعلية: >>> import wizcoin >>> purse = wizcoin.WizCoin(2, 5, 10) >>> purse * 10 # يستدعي __mul__() بقيمة 10 للمعامل other WizCoin(20, 50, 100) >>> 10 * purse # يستدعي __rmul__() بقيمة 10 للمعامل other WizCoin(20, 50, 100) تذكر أن بايثون تستدعي في التعبير 10 * purse تابع ()__mul__ الخاص بالصنف int لمعرفة ما إذا كان بإمكان العدد الصحيح أن يُضرب بكائنات WizCoin. لا يعلم طبعًا صنف بايثون int المبني مسبقًا أي شيء عن الأصناف التي أنشأناها، لذا تعيد NotImplemented. هذا يشير لبايثون باستدعاء التابع ()__rmul__ الخاص بصنف WizCoin، وإذا كان موجودًا للتعامل مع العملية الحسابية،ترفع بايثون استثناء TypeError إذا كان الاستدعائين للتابعين ()__mul__ و ()__rmul__ للصنفين Int و WizCoin على التتالي يعيدان NotImplemented. يمكن إضافة كائنات WizCoin إلى بعضها فقط، وهذا يضمن أن التابع الأول ()__add__ الخاص بالصنف WizCoin سيتعامل مع المعامل لذا لا نحتاج لتنفيذ ()__radd__. مثلًا، في التعبير purse + tipJar يُستدعى التابع ()__add__ للكائن purse وتمرّر tipJar للمعامل other. لا تحاول بايثون استدعاء تابع ()__radd__ الخاص بالكائن tipJar لأن هذا الاستدعاء لن يعيد NotImplemented، وتكون purse هي المعامل other. يحتوي الجدول 2 على قائمة كاملة للتوابع السحرية العددية المعكوسة. التابع السحري العملية المعامل أو الدالة المضمنة ()__radd__ جمع + ()__rsub__ طرح - ()__rmul__ ضرب * ()__rmatmul__ ضرب المصفوفات (جديد في بايثون 3.5) @ ()__rtruediv__ قسمة / ()__rfloordiv__ قسمة عدد صحيح // ()__rmod__ نسبة % ()__rdivmod__ قسمة ونسبة divmode() ()__rpow__ رفع للأس pow, ** ()__rlshift__ انتقال لليسار << ()__rrshift__ انتقال لليمين >> ()__rand__ عملية ثنائية و & ()__ror__ عملية ثنائية أو | ()__rxor__ عملية ثنائية أو حصرية ^ الجدول 2: التوابع السحرية العددية المعكوسة التوثيق الكامل للتوابع السحرية المعكوسة موجود في توثيقات بايثون. الخلاصة تسمح لك بايثون بإعادة تعريف العوامل باستخدام التوابع السحرية التي تبدأ وتنتهي بمحرفي شرطة سفلية، كما يمكن إعادة صياغة العوامل الرياضية الشائعة باستخدام التوابع السحرية العددية والمعكوسة، إذ تقدم هذه التوابع طريقة لعمل عوامل بايثون الموضعية مع كائنات الأصناف التي أنشأتها وإذا لم تكن قادرة على التعامل مع نوع بيانات الكائن على الطرف الأخر من المعامل ستعيد قيمة NotImplemented المبنية مسبقًا. تُنشئ هذه التوابع السحرية وتعيد كائنات جديدة، في حين تُعدل التوابع السحرية الموضعية (التي تُعيد تعريف معاملات الإسناد المدعومة) الكائنات موضعيًا. لا تنفذ التوابع السحرية المقارنة معاملات بايثون الستة للمقارنة فقط، ولكن تسمح لدالة بايثون sort() بترتيب كائنات الأصناف الخاصة بك. ستحتاج لاستخدام الدوال eq() و ne() و lt() و le() و gt() و ge() في وحدة العامل لمساعدتك في تنفيذ هذه التوابع السحرية. تسمح الخواص والتوابع السحرية بكتابة الأصناف الخاصة بك بطريقة متناسقة وقابلة للقراءة، كما تسمح لك بتفادي الشيفرة النمطية التي تتطلبها لغات البرمجة الأخرى مثل جافا. لتعلم كتابة شيفرة بايثون هناك حديثان لريموند هيتغير Raymond Hettiger يتوسعان في هذه الأفكار "تحويل الشيفرة إلى بايثون اصطلاحية". و"ما وراء PEP 8 - أفضل الممارسات لشيفرة جميلة وواضحة" التي تغطي بعض المفاهيم التي ذكرناها وأكثر. ترجمة -وبتصرف- لقسم من الفصل Pythonic OOP: Properities and dunder methods من كتاب Beyond the Basic Stuff with Python. اقرأ المزيد المقال السابق البرمجة كائنية التوجه في بايثون: الخاصيات Properties. الدوال الرياضية المضمنة في بايثون 3. توثيق بايثون.
-
استعرضنا في المقال السابق مفهوم الوراثة في البرامج كائنية التوجه، سنتابع في هذا المقال الموضوع ذاته إذ سنستعرض بعض التوابع المهمة بهذا الخصوص، إضافةً إلى مناقشة مفهوم الوراثة المتعددة الموجودة في لغة بايثون. الدالتين isinstance() و isssubclass() يمكننا تمرير الكائن عندما نريد معرفة نوعه إلى الدالة type() المضمنة كما تحدثنا سابقًا، ولكن إذا أردنا التحقق من نوع كائن فيُفضل استخدام الدالة المبنية مسبقًا isinstance()، التي تعيد الدالة قيمة True إذا كان الكائن في الصنف المعطى أو صنفها الفرعي. اكتب ما يلي في الصدفة التفاعلية: >>> class ParentClass: ... pass ... >>> class ChildClass(ParentClass): ... pass ... >>> parent = ParentClass() # إنشاء كائن ParentClass >>> child = ChildClass() # إنشاء كائن ChildClass >>> isinstance(parent, ParentClass) True >>> isinstance(parent, ChildClass) False 1 >>> isinstance(child, ChildClass) True 2 >>> isinstance(child, ParentClass) True لاحظ أن isinstance() تشير إلى أن كائن ChildClass في child هو نسخةٌ من ChildClass (السطر ذو الرقم 1) ونسخةٌ من ParentClass (السطر ذو الرقم 2)، وهذا منطقي لأن كائن ChildClass له علاقة من نوع "is a" مع نوع كائن ParentClass، أي أنه نوع من هذا الكائن. يمكن أيضًا تمرير صف tuple من كائنات الأصناف مثل وسيط ثانٍ لمعرفة إذا كان الوسيط الأول هو واحد من الأصناف الموجودة في الصف: # تُعيد True إذا كانت القيمة 42 عددًا صحيحًا أو سلسلة نصية أو قيمة بوليانية >>> isinstance(42, (int, str, bool)) True if 42 is an int, str, or bool. True الدالة المضمنة الأخرى issubclass() أقل شيوعًا من isinstance() ويمكنها التعرُّف ما إذا كان كائن الصنف الممر إلى الوسيط الأول هو صنف فرعي (أو نفس الصنف) لكائن الصنف المرر إلى الوسيط الثاني: >>> issubclass(ChildClass, ParentClass) # ChildClass صنف فرعي من ParentClass True >>> issubclass(ChildClass, str) # ChildClass ليس صنفًا فرعيًا من من str False >>> issubclass(ChildClass, ChildClass) # ChildClass هو ChildClass True يمكنك تمرير صف من كائنات الصنف بمثابة وسيط ثاني إلى issubclass() كما هو الحال مع Isinstance()، وذلك لرؤية ما إذا كان الوسيط الأول هو صنف فرعي لأي من الأصناف في الصف. الفارق الأساسي بين isinstance() و issubclass() هو أن issubclass() تمرر كائني صنف و isinstance() تمرر كائن وكائن صنف. توابع الصنف ترتبط توابع الصنف مع صنف أكثر مقارنةً بالكائنات المفردة مثل التوابع العادية. يمكنك ملاحظة تابع الصنف في الشيفرة عندما ترى علامتين، هما: المزخرف @classmethod قبل تعليمة التابع def، واستخدام cls معاملًا أولًا كما في المثال التالي: class ExampleClass: def exampleRegularMethod(self): print('This is a regular method.') @classmethod def exampleClassMethod(cls): print('This is a class method.') # استدعاء تابع الصنف دون إنشاء نسخة كائن ExampleClass.exampleClassMethod() obj = ExampleClass() # بالنظر إلى السطر السابق، السطرين التاليين متكافئين obj.exampleClassMethod() obj.__class__.exampleClassMethod() يعمل المعامل cls مثل self ولكن self تشير إلى كائن بينما يشير المعامل cls إلى صنف الكائن، هذا يعني أن الشيفرة في تابع الصنف لا يمكنها الوصول إلى خاصيات الكائن المفردة أو استدعاء توابع الكائن العادية. تستدعي توابع الأصناف توابع أصناف أخرى وتستطيع الوصول إلى سمات الصنف. نستخدم الاسم cls لأن class هي كلمة مفتاحية في بايثون وكما هو الحال مع باقي الكلمات المفتاحية مثل if و while و import، فنحن لا نستطيع استخدامها في أسماء المعاملات، ونستدعي غالبًا سمات الأصناف من خلال كائن الصنف، مثل ExampleClass.exampleClassMethod()، إلا أنه يمكننا استدعاؤهم من خلال أي كائن من الصنف كما في obj.exampleClassMethod(). لا تُستخدم توابع الصنف عمومًا وأكثر الحالات استخدامًا هي لتوفير بديل عن توابع الباني constructorإضافةً للتابع __init__(). على سبيل المثال، ماذا لو كانت دالة الباني تقبل سلسةً نصيةً من البيانات يحتاجها الكائن الجديد أو سلسلة نصية لاسم ملف يحتوي البيانات التي يحتاجها الكائن الجديد؟ لا نحتاج إلى قائمة معاملات التابع ()__init__ لأنها ستكون طويلة ومعقدة، ونستخدم تابع دالة يعيد كائن جديد بدلًا من ذلك. مثلًا، لننشئ صنف AsciiArt (مررنا عليه سابقًا) الذي يستخدم محارف نصية ليشكل صورة: class AsciiArt: def __init__(self, characters): self._characters = characters @classmethod def fromFile(cls, filename): with open(filename) as fileObj: characters = fileObj.read() return cls(characters) def display(self): print(self._characters) # Other AsciiArt methods would go here... face1 = AsciiArt(' _______\n' + '| . . |\n' + '| \\___/ |\n' + '|_______|') face1.display() face2 = AsciiArt.fromFile('face.txt') face2.display() لدى صنف AsciiArt تابع ()__init__ الذي يمكن أن يمرر محارف النص الصورة مثل سلسلة نصية. لديه أيضًا تابع صنف fromFile() الذي يمكن أن يمرر السلسلة النصية لاسم الملف مثل ملف نصي يحتوي فن آسكي ASCII art. يُنشئ كلا التابعين كائنات AsciiArt. نفذ البرنامج وسيكون هناك ملف face.txt يحتوي على وجه فن آسكي ASCII، ليكون الخرج على النحو التالي: _______ | . . | | \___/ | |_______| _______ | . . | | \___/ | |_______| يجعل تابع الصنف fromFile() الشيفرة الخاصة بك سهلة القراءة مقارنةً بجعل ()__init__ يفعل كل شيء. ميزة أُخرى لتابع الصنف هو أن صنف فرعي من AsciiArt يمكن أن يرث تابع fromFile() الخاص (وإعادة تعريفه إذا لزم)، وهذا هو سبب استدعاء cls(characters) في تابع صنف AsciiArt بدلًا من AsciiArt(characters). يعمل استدعاء ()cls أيضًا في الأصناف الفرعية للصنف AsciiArt دون تعديل لأن صنف AsciiArt ليس متوفرًا في التابع، ولكن استدعاء AsciiArt() يستدعي ()__init__ الخاص بصنف AsciiArt بدلًا من ()__init__ الخاص بالصنف الفرعي. يمكنك التفكير في cls على أنها "كائن يمثل هذا الصنف". خذ بالحسبان أنه يجب أن تستخدم التوابع العادية معامل self في مكان ما في الشيفرة الخاصة بهم، ويجب على تابع الصنف دائمًا استخدام المعامل cls. إذا لم يستخدم أبدًا تابع الصنف المعامل cls، فهذه إشارة أن تابع الصنف الخاص بك يجب أن يكون تابعًا عاديًا. سمات الأصناف سمة الصنف هي متغير ينتمي إلى صنف بدلًا من كائن. ننشئ سمة صنف داخل الصنف ولكن خارج كل التوابع كما أنشأنا متغيرات عامة في ملف ".py" ولكن خارج كل الدوال. هذا مثال عن سمة صنف اسمها count التي تحصي عدد كائنات CreatCounter المُنشأة. class CreateCounter: count = 0 # هذه سمة لصنف def __init__(self): CreateCounter.count += 1 print('Objects created:', CreateCounter.count) # تطبع 0 a = CreateCounter() b = CreateCounter() c = CreateCounter() print('Objects created:', CreateCounter.count) # تطبع 3 لدى صنف CreatCounter سمة صنف واحدة اسمها count. كل كائنات CreatCounter لديهم هذه السمة بدلًا من أن يكون لكل منهم سمات count منفصلة. لهذا يعد السطر CreateCounter.count += 1 في دالة الباني كل كائن CreatCounter مُنشأ. عندما تنفذ البرنامج، يكون الخرج على النحو التالي. Objects created: 0 Objects created: 3 نادرًا ما نستخدم سمات الصنف حتى هذا المثال "عد كم كائن CreatCounter مُنشأ" يمكن عمله باستخدام متغير عام بدلًا من سمة صنف. التوابع الساكنة لا يحتوي التابع الساكن معاملي self و cls، فالتوابع الساكنة هي دوال لأنها لا تستطيع الوصول إلى سمات أو توابع الصنف وكائناتها. نادرًا ما ستحتاج لاستخدام التوابع الساكنة في بايثون، وإذا قررت إنشاء واحد ننصح جدًا بإنشاء تابع عادي بدلًا عنه. نعرّف التوابع الساكنة بوضع مزخرف @staticmethod قبل تعليمة def الخاصة بهم. هذا مثال عن تابع ساكن: class ExampleClassWithStaticMethod: @staticmethod def sayHello(): print('Hello!') # لم يُنشأ أي كائن، فاسم الصنف يسبق sayHello() Note that no object is created, the class name precedes sayHello(): ExampleClassWithStaticMethod.sayHello() لا يوجد فرق تقريبًا بين التابع الساكن sayHello() في صنف ExampleClassWithStaticMethod والدالة sayHello(). ربما تفضل بالواقع استخدام دالة لأنك تستطيع استدعائها دون الدخول إلى اسم الصنف مسبقًا. التوابع الساكنة شائعة في لغات برمجة أخرى ليس لديها ميزات لغة بايثون المرنة. تضمين التوابع الساكنة inclusion of static methods في بايثون هو لمحاكاة اللغات الأخرى ولا يقدم قيمةً عملية. متى تستخدم الأصناف والميزات كائنية التوجه الساكنة؟ نادرًا ما تحتاج لاستخدام توابع الصنف وسمات الصنف والتوابع الساكنة، فهم عرضةً للاستخدام الزائد إذا كنت تعتقد بالتساؤل التالي: " لماذا لا استخدم الدوال أو المتغيرات العامة بدلًا عن ذلك؟" هذا تلميح لعدم استخدام توابع الأصناف أو سمات الأصناف أو التوابع الساكنة. السبب الوحيد الذي جعلنا نناقش هذه المفاهيم في سلسلة المقالات متوسطة المستوى هو للتعرف عليهم عندما تراهم في الشيفرة، ولكن لا يشجع كثير من المبرمجين على استخدامهم، إذ سيكونوا مفيدين إذا أردت استخدام هيكلية خاصة بك مع مجموعة معقدة من الأصناف التي تتوقع أن تكون أصناف فرعية للمبرمجين الذين سيستخدمون الهيكلية، لكنك لا تحتاجهم عندما تكتب تطبيقات بايثون مباشرةً. للمزيد عن هذه الميزات وعن احتياجهم أو لا، اقرأ منشور فيليب ج. ايبي Phillip J. Eby "بايثون ليس جافا" الموجود على الرابط dirtsimple.org/2004/12/python-is-not-java.html ومنشور ريان تومايكو Ryan Tomayko "مفهوم التابع الساكن" على الرابط tomayko.com/blog/2004/the-static-method-thing. كلمات مهمة كائنية التوجه يبدأ شرح البرمجة كائنية التوجه OOP بالكثير من المصطلحات مثل الوراثة والتغليف Encapsulation والتعددية الشكلية Polymorphism. أهمية معرفة هذه المصطلحات مبالغ فيه، ولكن يجب عليك أن يكون لديك فهم أساسي لهم، شرحنا الوراثة سابقًا، لذا سنشرح المصطلحات الباقية تاليًا، ويمكنك الاطلاع على مقال البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C على أكاديمية حسوب لمزيدٍ من المعلومات حول هذه المصطلحات. التغليف لدى كلمة تغليف معنيين شائعين ولكن متقاربين. التعريف الأول هو تجميع البيانات المتعلقة والشيفرة في وحدة واحدة، أي لتغلف يعني أن تضع في صندوق. هذا ما تفعله الأصناف عمومًا؛ فهي تدمج السمات والتوابع. مثلًا، يغلف صنف WizCoin ثلاثة أعداد صحيحة لـ knuts و sickles و galleons إلى كائن WizCoin واحد. أما التعريف الثاني فهو تقنية لإخفاء المعلومات تسمح للكائنات بإخفاء تفاصيل تنفيذ معقدة عن كيفية عمل الكائنات. رأينا ذلك في "السمات والتوابع الخاصة"، إذ يقدم كائن BankAccount توابع deposit() و withdraw() لإخفاء تفاصيل كيفية التعامل مع السمة _balance. تعمل الدوال مثل صندوق أسود: كيفية حساب الدالة math.sqrt() للجذر التربيعي لأي رقم مخفية، كل ما عليك معرفته هو أن تعيد الدالة الجذر التربيعي للرقم الممرر لها. التعددية الشكلية polymorphism تسمح التعددية الشكلية بمعالجة كائنات من نوع ما على أنها كائنات من نوع آخر، فمثلًا تعيد الدالة ()len طول الوسيط الممرر إليها، ويمكنك تمرير سلسلة نصية إلى هذه الدالة لمعرفة عدد المحارف المكونة منه، وكذلك يمكن قائمة list أو قاموس dictionary لمعرفة عدد العناصر، أو عدد أزواج مفتاح-قيمة key-value على الترتيب. يدعى نموذج التعددية الشكلية هذا باسم الدوال المعممة generic functions أو التعددية الشكلية القياسية parametric polymorphism لأنها تعالج كائنات ذات أنواع مختلفة. يمكنك الاطلاع على مقال مفهوم البرمجة المعممة Generic Programming على أكاديمية حسوب لمزيدٍ من المعلومات عن البرمجة المعممة. يمكن الإشارة إلى التعددية الشكلية بمصطلح التعددية الشكلية الخاصة ad hoc polymorphism أو زيادة تحميل للعامل operator overloading، إذ يمكن أن يأخذ المعامل (مثل + أو *) سلوكًا مختلفًا اعتمادًا على نوع الكائنات التي تُجرى عليها العملية؛ فمثلًا يجري المعامل + عملية الجمع الحسابي عندما تُجرى العملية على عددين صحيحين أو عشريين، لكنها توصل السلاسل النصية في حال كانت العملية على سلسلتين بدلًا من عددين. لماذا لا نستخدم الوراثة؟ من السهل زيادة تعقيد الأصناف باستخدام الوراثة. كما يشير لوتشيانو رامالهو Luciano Ramalho : "وضع الكائنات في هرمية يرضي حس الترتيب لدينا، ولكن المبرمجين يفعلونها للتسلية". ننشئ أصناف وأصناف فرعية وأصناف تحت فرعية عندما تكون الحاجة لصنف واحد أو اثنين في الوحدة لتفي بالغرض، ولكن تذكر حكمة بايثون الأهم التي ناقشناها سابقًا؛ الحل البسيط أفضل من الحل المعقد. يسمح استخدام البرمجة كائنية التوجه OOP بتنظيم الشيفرة الخاصة بك إلى وحدات units (في هذه الحالة أصناف) سهلة التعامل بدلًا من ملف ".py" واحد يحتوي مئات التوابع المعرفة بدون ترتيب معين. تفيد الوراثة إذا كان لديك عدة دوال تعمل في نفس القاموس أو هيكل قائمة البيانات؛ ففي هذه الحالة من المفيد ترتيبهم في صنف. هناك بعض الأمثلة لعدم إنشاء الأصناف أو استخدام الوراثة: إذا كان الصنف يتألف من توابع لا تستخدم المعاملين self و cls، احذف الأصناف واستخدم الدوال بدلًا من التوابع. إذا أنشأت أب بصنف ابن واحد ولم تُنشئ كائنات من الصنف الأب، يمكنك جمعهم بصنف واحد. إذا أنشأت أكثر من ثلاثة أو أربعة مستويات من الأصناف الفرعية، ربما تكون قد استخدمت الوراثة على نحوٍ زائد، اجمع هذه الأصناف الفرعية إلى أصناف أقل. كما توضّح سابقًا في نسختي برنامج إكس أو tic-tac-toe (مع برمجة كائنية التوجه وبدونها)، من الممكن الحصول على برنامج يعمل على نحوٍ سليم وبدون أخطاء دون استخدام الأصناف. لست بحاجة لتصميم برنامج مثل شبكة معقدة من الأصناف، إذ أن الحل البسيط أفضل من الحل المعقد الذي لا يعمل. يتحدث جول سبلوسكي Joel Spolsky عن ذلك في منشوره "لا تدع المصممين رواد الفضاء أن يخيفوك" الموجود على الرابط joelonsoftware.com/2001/04/21/dont-let-architecture-astronauts-scare-you. يجب أن تعرف الآن كيفية عمل مفاهيم البرمجة كائنية التوجه مثل الوراثة، لأنها تساعدك على تنظيم الشيفرة الخاصة بك وجعل التطوير ومعالجة الأخطاء أسهل. تتمتع لغة بايثون بالمرونة، فهي تقدم لك ميزات برمجة كائنية التوجه، لكنها أبضًا لا تطلب منك استخدامها عندما لا تناسب احتياجات البرنامج الخاص بك. الوراثة المتعددة في العديد من لغات البرمجة يكون الصنف أب واحد فقط، ولكن بايثون تدعم آباء متعددين عن طريق تقديم ميزة تدعى الوراثة المتعددة multiple inheritance. مثلًا، يمكننا الحصول على صنف Airplane مع تابع flyInTheAir() وصنف Ship مع تابع floatOnWater()، ويمكننا إنشاء صنف FlyingBoat يرث كلًا من Airplane و Ship عن طريق تحديدهما في تعليمة class مفصولين بفواصل. افتح ملف جديد في محرر النصوص واحفظ التالي flayingboat.py: class Airplane: def flyInTheAir(self): print('Flying...') class Ship: def floatOnWater(self): print('Floating...') class FlyingBoat(Airplane, Ship): pass سيرث الكائن المُنشأ التابعين flyInTheAir() و floatOnWater() كما سنرى في الصدفة التفاعلية: >>> from flyingboat import * >>> seaDuck = FlyingBoat() >>> seaDuck.flyInTheAir() Flying... >>> seaDuck.floatOnWater() Floating... الوراثة المتعددة مفهوم بسيط طالما كانت أسماء توابع الأصناف مميزة ولا تتقاطع، وتسمى هذه الأصناف mixins (هذا مصطلح عام لهذا النوع من الأصناف، إذ لا يوجد في بايثون كلمة mixin مفتاحية)، ولكن ماذا سيحصل إذا ورثنا عدة أصناف معقدة تتشارك بأسماء التوابع؟ مثلًا تذكر أصناف لوحة إكس أو MiniBoard و HintTTTBoard سابقًا، ماذا لو أردنا صنف يظهر لوحة إكس أو مصغرة مع تقديم بعض النصائح؟ يمكننا إعادة استخدام هذه الأصناف الموجودة باستخدام الوراثة المتعددة. ضِف التالي إلى نهاية ملف tictactoe_oop.py ولكن قبل تعليمة if التي تستدعي الدالة main(): class HybridBoard(HintBoard, MiniBoard): pass لا يوجد شيء في هذا الصنف، إذ يُعيد استخدام الشيفرة عن طريق وراثة HintBoard و MiniBoard. عدّل الشيفرة في الدالة main() لتُنشئ كائن HybridBoard: gameBoard = HybridBoard() # إنشاء كائن TTT للّوحة لدى كلا الصنفين الأب MiniBoard و HintBoard تابع اسمه getBoardStr() فما الذي ترثه HybridBoard؟ عندما تنفذ البرنامج سيظهر الخرج لوحة إكس أو مصغرة تحتوي على بعض التلميحات: --snip-- X.. 123 .O. 456 X.. 789 X can win in one more move. يبدو أن بايثون دمجت سحريًا تابع getBoardStr() الخاص بصنف MiniBoard و getBoardStr() الخاص بصنف HintBoard، وهذا ممكن لأننا كتبنا التابعين بشكل يمكّنهما العمل مع بعضهما. إذا بدلت ترتيب الأصناف في تعليمة class في صنف HybridBoard لتصبح على النحو التالي: class HybridBoard(MiniBoard, HintBoard): فستخسر التلميحات كليًا: --snip-- X.. 123 .O. 456 X.. 789 لتفهم لماذا حصل ذلك يجب عليك فهم ترتيب استبيان التوابع method resolution order -أو اختصارًا MRO- الخاص ببايثون وكيفية عمل دالة super(). ترتيب استبيان التابع لدى برنامج إكس أو الخاص بنا أربعة أصناف لتمثيل الألواح، ثلاثة معرفة بتابع getBoardStr() وواحدة بتابع getBoardStr() موروث كما في الشكل 2 [الشكل 2: الأصناف الأربعة في برنامج لوحات إكس أو] عندما نستدعي getBoardStr() على الكائن HybridBoard، يعرف بايثون أن الصنف HybridBoard ليس لديه تابع بذلك الاسم لذا تفحص أصناف الأب، ولكن لدى الصنف هذا صنفين أب وكلاهما لديه تابع getBoardStr()، أي منها يُستدعى؟ يمكننا معرفة ذلك من التحقق من ترتيب استبيان التابع MRO الخاص بصنف HybridBoard وهي القائمة المرتبة من الأصناف التي يتحقق منها بايثون عند وراثة التوابع، أو عندما يستدعي التابع دالة super(). يمكنك رؤية ترتيب استبيان التابع للصنف HybridBoard عن طريق استدعاء mro() في الصدفة التفاعلية: >>> from tictactoe_oop import * >>> HybridBoard.mro() [<class 'tictactoe_oop.HybridBoard'>, <class 'tictactoe_oop.HintBoard'>, <class 'tictactoe_oop.MiniBoard'>, <class 'tictactoe_oop.TTTBoard'>, <class 'object'>] يمكنك من خلال القيمة المُعادة رؤية أنه عندما يُستدعى التابع على HybridBoard، يتحقق بايثون من صنف HybridBoard؛ فإذا لم يكن موجودًا، يتحقق بايثون من صنف HintBoard وبعدها من صنف MiniBoard وأخيرًا من صنف TTTBoard. في آخر كل قائمة ترتيب استبيان الدوال MRO هناك صنف object مضمّن يمثل الصنف الأب لكل الأصناف في بايثون. معرفة ترتيب استبيان الدوال MRO من أجل وراثة واحدة أمر سهل؛ فقط اصنع سلسلة chain من أصناف الأب، أما بالنسبة للوراثة المتعددة سيكون الأمر أصعب. يتبع ترتيب استبيان الدوال MRO الخاص ببايثون خوارزمية C3 -التي تقع تفاصيل مناقشتها خارج سياق موضوعنا- ولكنك تستطيع تحديد ترتيب استبيان الدوال MRO بتذكر قاعدتين: يتحقق بايثون من الأصناف الابن قبل أصناف الأب. يتحقق من الأصناف الموروثة في القائمة من اليسار إلى اليمين في تعليمة class. إذا استدعينا getBoardStr() على كائن HybridBoard، يتحقق بايثون من الصنف HybridBoard أولًا وبعدها ونظرًا لكون أصناف الأب من اليسار إلى اليمين هي HintBoard و MiniBorad، يتحقق بايثون من HintBoard. لدى الصنف الأب هذا تابع getBoardStr() لذا يرثها HybridBorad ويستدعيها. لا ينتهي الأمر هنا، يستدعي التابع super().getBoardStr()، إذ أن كلمة "super" هي كلمة مضللة نوعًا ما لدالة super() الخاصة ببايثون، لأنها لا تعيد الصنف الأب ولكن الصنف الذي يليها في ترتيب استبيان التوابع MRO، وهذا يعني عندما نستدعي getBoardStr() على الكائن HybridBoard، يكون الصنف التالي في ترتيب استبيان التوابع MRO بعد HintBoard هو MiniBoard وليس الصنف الأب TTTBoard، لذا استدعاء super().getBoardStr() يستدعي تابع getBoardStr() لصنف MiniBoard الذي يعيد سلسلة نصية للوحة إكس أو المصغرة. تعلّق الشيفرة المتبقية في getBoardStr() الخاصة بصنف HintBoard بعد استدعاء super() نص التلميح لهذه السلسة النصية. إذا غيرنا تعليمة class في صنف HybridBoard لتضع MiniBoard أولًا و HintBoard ثانيًا، سيضع ترتيب استبيان التوابع MRO الصنف MiniBoard قبل الصنف HintBoard، ما يعني أن HybridBorad ترث getBoardStr() من MiniBoard التي لا تحتوي استدعاء super(). هذا الترتيب هو الذي سبّب الخطأ الذي جعل لوحة إكس أو المصغرة تظهر بدون تلميحات؛ فبدون استدعاء super() تابع getBoardStr() الخاص بصنف MiniBoard لا يستدعي تابع getBoardStr() الخاص بصنف HintBoard. تسمح لك الوراثة المتعددة في إنشاء وظائف كثيرة في كمية قليلة من الشيفرة، لكنها تقود إلى شيفرة معقدة وصعبة القراءة. فضّل الوراثة الواحدة أو أصناف mixin أو عدم الوراثة، هذه التقنيات غالبًا ما تكون قادرة على تنفيذ مهام البرنامج الخاص بك. الخلاصة كما تعيد type() نوع الكائن المرر لها، تعيد توابع isinstace() و issubclass() نوع ومعلومات الوراثة عن الكائن الممرر لها. يمكن أن تحتوي الأصناف توابع كائن وسمات، لكنها تحتوي أيضًا توابع صنف وسمات صنف وتوابع ساكنة، على الرغم من أنها نادرة الاستخدام لكن يمكنها أن تسمح بالتقنيات كائنية التوجه التي لا تستطيع المتغيرات العامة والدوال أن تقدمها. بسمح بايثون للأصناف أن ترث من عدة آباء، على الرغم من أن ذلك ينتج شيفرة صعبة الفهم. تستطيع دالة super() وتوابع الصنف اكتشاف كيف سترث التوابع اعتمادًا على ترتيب استبيان التوابع MRO، إذ يمكنك مشاهدة ترتيب استبيان التوابع MRO الخاص بصنف في الصدفة التفاعلية عن طريق استدعاء التابع mro() على الصنف. غطينا في هذا المقال والمقالات السابقة مفاهيمًا عامة في البرمجة كائنية التوجه OOP، وسنتحدث تاليًا عن تقنيات برمجة كائنية التوجه OOP خاصة ببايثون. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Inheritance من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق البرمجة كائنية التوجه Object-Oriented Programming والوراثة Inheritance البرمجة كائنية التوجه (Object Oriented Programming) في بايثون البرمجة كائنية التوجه (Object Oriented Programming) في لغة سي شارب #C الوراثة المتعددة multiple inheritance
-
يوفر عليك استدعاء وتعريف الدوال من عدة أماكن نسخ ولصق الشيفرة المصدرية، إذ أن عدم تكرار الشيفرة هو ممارسة جيدة لأنه إذا أردت تغيير هذه الشيفرة المكرّرة (إما لحل بعض الأخطاء أو لإضافة ميزات جديدة)، فستحتاج فقط لتغييرها في مكان واحد، ويصبح البرنامج أقصر دون شيفرة مكررة وأسهل للقراءة. الأمر مماثل بالنسبة للدوال، الوراثة inheritance هي تقنية لإعادة استخدام الشيفرة، ويمكن تطبيقها في الأصناف، وهي وسيلة لوضع الأصناف في علاقة أب-ابن، بحيث يرث الصنف الابن نسخةً من توابع الصنف الأب، ويخفف عليك عبء تكرار التوابع في عدة أصناف. يعتقد العديد من المبرمجين أن الوراثة أمرٌ مبالغٌ فيه أو خطر بسبب زيادة تعقيد شبكات وراثة الأصناف المضافة إلى البرنامج. ليست المنشورات المدونة بعنوان "الوراثة خطيرة" خاطئة كليًا، فمن السهل استغلال البرنامج، ولكن الاستخدام المحدود لهذه التقنية يمكن أن يوفر الكثير من الوقت بما يتعلق بتنظيم الشيفرة. كيف تعمل الوراثة نضع اسم الصنف الأب الموجود أساسًا بين قوسين في تعليمة class لإنشاء صنف ابن. لتتدرب على إنشاء صنف ابن، نفتح نافذة محرر الملفات ونكتب الشيفرة التالية ونحفظها في ملف inheritanceExample.py: 1 class ParentClass: 2 def printHello(self): print('Hello, world!') 3 class ChildClass(ParentClass): def someNewMethod(self): print('ParentClass objects don't have this method.') 4 class GrandchildClass(ChildClass): def anotherNewMethod(self): print('Only GrandchildClass objects have this method.') print('Create a ParentClass object and call its methods:') parent = ParentClass() parent.printHello() print('Create a ChildClass object and call its methods:') child = ChildClass() child.printHello() child.someNewMethod() print('Create a GrandchildClass object and call its methods:') grandchild = GrandchildClass() grandchild.printHello() grandchild.someNewMethod() grandchild.anotherNewMethod() print('An error:') parent.someNewMethod() عندما ننفذ البرنامج، يكون الخرج على النحو التالي: Create a ParentClass object and call its methods: Hello, world! Create a ChildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Create a GrandchildClass object and call its methods: Hello, world! ParentClass objects don't have this method. Only GrandchildClass objects have this method. An error: Traceback (most recent call last): File "inheritanceExample.py", line 35, in <module> parent.someNewMethod() # ParentClass objects don't have this method. AttributeError: 'ParentClass' object has no attribute 'someNewMethod' أنشأنا ثلاثة أصناف ParentClass في السطر (1) و ChildClass في السطر (3) و GrandchildClass في السطر (4). الصنف ChildClass هو صنف فرعي للصنف ParentClass، يعني أن ChildClass لديها توابع ParentClass نفسها، ونقول أن ChildClass يرث التوابع من ParentClass، وأيضًا GrandchildClass هو صنف فرعي من ChildClass وبالتالي لديه كل توابع ChildClass وأبيها ParentClass. نسخنا ولصقنا الشيفرة من التابع printHello() باستخدام هذه الطريقة إلى الصنفين ChildClass و GrandChild. أي تغيير للشيفرة في PrintHello() لا يحدث فقط في ParentClass بل في ChildClass و GrandchildClass. هذا نفس تغيير الشيفرة في دالة تُحدّث كل استدعاءات الدالة الخاصة بها. يمكنك رؤية هذه العلاقة في الشكل 1. لاحظ في مخططات الأصناف أن السهم ينطلق من الصنف الفرعي ويشير إلى الصنف الأساس. هذا يعكس أن كل صنف يعرف دائمًا الصنف الأساس الخاص به ولكنه لا يعرف أصنافه الفرعية. [الشكل 1: مخطط هرمي (يسار) ومخطط فين Venn (يمين) يبينان العلاقات بين الأصناف الثلاثة والتوابع التي يستخدموها] تمثّل الأصناف أب- ابن عادةً علاقات "is a"، إذ أن كائن ChildClass هو كائن ParentClass لأن لديه نفس التوابع التي لدى كائن ParentClass، إضافةً إلى بعض التوابع الإضافية التي يعرّفها. هذه هي علاقة باتجاه واحد: ليس كائن ParentClass هو كائن ChildClass. إذا حاول كائن استدعاء someNewMethod() الموجود فقط لكائنات ChildClass (وأصناف ChildClass الفرعية) يعطي بايثون Python خطأ AttributeError. يمكنك الاطلاع على مقال مخططات الفئات (Class Diagram) في لغة النمذجة الموحدة UML على أكاديمية حسوب لمزيدٍ من المعلومات على علاقات "is a" وغيرها في مخططات الأصناف. يعتقد المبرمجون أن الأصناف المتعلقة ببعضها تندرج تحت هرمية علاقات "is a" واقعية، فغالبًا ما ترى في تدريبات البرمجة كائنية التوجه OOP أصناف أب وابن وحفيد: Vehicle▶FourWheelVehicle▶Car أو Animal▶Bird▶Sparrow أو Shape▶Rectangle▶Square لكن تذكر أن السبب الأساسي للوراثة هو إعادة استعمال الشيفرة. تسمح لك الوراثة بتفادي نسخ ولصق الشيفرة إذا كان البرنامج الخاص بك يحتاج إلى صنف بمجموعة من التوابع التي هي مجموعة كبرى من توابع صنف أُخر. نسمي أحيانًا الصنف الابن بالصنف الفرعي subclass أو الصنف المُشتق derived class ونسمي الصنف الأب الصنف الأعلى super class أو الصنف الأساس base class، ويمكنك الاطلاع على مقال الوراثة والتعددية الشكلية Polymorphism والأصناف المجردة Abstract Classes في جافا على أكاديمية حسوب لمزيدٍ من المعلومات. إعادة تعريف التوابع ترث الأصناف الفرعية كل توابع الأصناف الأب، ويمكن لصنف ابن إعادة تعريف تابع موروث عن طريق تقديم التابع والشيفرة الخاصين بها. يكون لدى التابع الذي يعيد التعريف اسم تابع الصنف الأب ذاته. لتوضيح هذا المفهوم لنعد إلى لعبة إكس أو Tic-Tac-Toe التي أنشأناها سابقًا، ولكن هذه المرة سننشئ صنفًا جديدًا MiniBoard وهو صنف فرعي من TTTBoard يعيد تعريف getBoardStr() لرسم لوحة إكس أو أصغر. سيسأل البرنامج أي نوع لوح سيستخدم ولا نحتاج إلى نسخ ولصق باقي توابع TTTBoard لأن MiniBoard سيرثهم. ضِف التالي في نهاية ملف ticktactoe_oop.py لإنشاء صنف ابن لصنف TTTBoard الأصلي، ثم أعد كتابة تابع getBoardStr(): class MiniBoard(TTTBoard): def getBoardStr(self): """Return a tiny text-representation of the board.""" # Change blank spaces to a '.' for space in ALL_SPACES: if self._spaces[space] == BLANK: self._spaces[space] = '.' boardStr = f''' {self._spaces['1']}{self._spaces['2']}{self._spaces['3']} 123 {self._spaces['4']}{self._spaces['5']}{self._spaces['6']} 456 {self._spaces['7']}{self._spaces['8']}{self._spaces['9']} 789''' # Change '.' back to blank spaces. for space in ALL_SPACES: if self._spaces[space] == '.': self._spaces[space] = BLANK return boardStr كما في التابع ()getBoardStr الخاص بصنف TTTBoard سينشئ التابع getBoardStr() الخاص بـ MiniBoard لإظهار سلسلة نصية متعددة الأسطر من لوحة إكس أو عندما تمرر إلى دالة print() ولكن هذه السلسلة النصية هي أقصر وتتجاهل الأسطر بين X و O وتستخدم الفواصل للدلالة على الأماكن الفارغة. غيّر السطر في main() ليستنسخ كائن MiniBoard بدلًا من كائن TTTBoard: if input('Use mini board? Y/N: ').lower().startswith('y'): gameBoard = MiniBoard() # Create a MiniBoard object. else: gameBoard = TTTBoard() # Create a TTTBoard object. يعمل البرنامج كما في السابق ما عدا تغيير هذا السطر الواحد في main() وعندما تنفذ البرنامج الآن سيصبح الخرج على النحو التالي: Welcome to Tic-Tac-Toe! Use mini board? Y/N: y ... 123 ... 456 ... 789 What is X's move? (1-9) 1 X.. 123 ... 456 ... 789 What is O's move? (1-9) --snip-- XXX 123 .OO 456 O.X 789 X has won the game! Thanks for playing! يستطيع البرنامج الآن بسهولة الحصول على تنفيذي صنفي لوح إكس أو، وإذا أردت فقط النسخة المصغرة من اللوحة يمكنك ببساطة استبدال الشيفرة في تابع getBoardStr() في TTTBoard. ولكن إذا أردت الاثنين فالوراثة تسمح لك بسهولة إنشاء صنفين عن طريق إعادة استخدام الشيفرة المشتركة بينهما. يمكننا إضافة سمة attribute جديدة إلى TTTBoard اسمها useMiniBoard إذا لم نستخدم الوراثة، ووضع تعليمة if-else داخل getBoardStr() لتقرر متى تُظهِر اللوحة العادية أو اللوحة المصغرة، سيعمل هذا جيدًا لأن التغيير بسيط، ولكن ماذا لو كان الصنف الفرعي MiniBoard يحتاج لإعادة تعريف تابعين أو ثلاثة توابع أو حتى 100 تابع؟ ماذا لو أردنا إنشاء عدة أصناف فرعية من TTTBoard؟ سيتسبّب عدم استخدام الوراثة بسيل من تعليمات if-else داخل التابع الخاص بنا وزيادة كبيرة في تعقيد الشيفرة. يُمكّننا استخدام الأصناف الفرعية وإعادة تعريف التوابع من ترتيب الشيفرة الخاصة بنا ضمن أصناف منفصلة للتعامل مع حالات استخدام مماثلة. دالة super() يشابه تابع الصنف المعاد تعريفه overridden تابع الصنف الأب؛ فحتى لو كانت الوراثة هي تقنية لإعادة استخدام الشيفرة، قد يتطلب إعادة تعريف التابع إعادة كتابة نفس الشيفرة من تابع الصنف الأب بمثابة جزء من تابع شيفرة الابن. لمنع تكرار الشيفرة: تسمح دالة super() للتابع المعاد تعريفه استدعاء التابع الأصلي في الصنف الأب. مثلًا، لنُنشئ صنفًا جديدًا اسمه HintBoard ليكون صنفًا فرعيًا من TTTBoard، بحيث يعيد هذا الصنف تعريف getBoardStr()، ويضيف بعد رسم لوحة إكس أو تلميحًا hint فيما إذا كان X أو O قد يربح في الخطوة التالية. هذا يعني أن تابع getBoardStr() الخاص بصنف HintBoard سينجز نفس مهام تابع getBoardStr() الخاص بصنف TTTBoard لرسم لوحة إكس أو. بدلًا من تكرار الشيفرة لإنجاز ذلك، يمكننا استخدام super() لاستدعاء تابع getBoardStr الخاص بصنف TTTBoard من تابع getBoardStr() الخاص بصنف HintBoard. ضِف التالي لنهاية ملف tictactoe_oop.ps: class HintBoard(TTTBoard): def getBoardStr(self): """Return a text-representation of the board with hints.""" 1 boardStr = super().getBoardStr() # Call getBoardStr() in TTTBoard. xCanWin = False oCanWin = False 2 originalSpaces = self._spaces # Backup _spaces. for space in ALL_SPACES: # Check each space: # Simulate X moving on this space: self._spaces = copy.copy(originalSpaces) if self._spaces[space] == BLANK: self._spaces[space] = X if self.isWinner(X): xCanWin = True # Simulate O moving on this space: 3 self._spaces = copy.copy(originalSpaces) if self._spaces[space] == BLANK: self._spaces[space] = O if self.isWinner(O): oCanWin = True if xCanWin: boardStr += '\nX can win in one more move.' if oCanWin: boardStr += '\nO can win in one more move.' self._spaces = originalSpaces return boardStr أولًا، تنفذ التعليمة super().getBoardStr() في السطر ذو الرقم 1 الشيفرة داخل الصنف getBoardStr() الخاص بصنف TTTBoard، والتي تعيد سلسلةً نصيةً على شكل لوحة إكس أو. نحفظ حاليًا هذه السلسلة في متغير اسمه boardStr. تعالج الشيفرة الباقية إنشاء التلميح بعد إنشاء لوحة السلسلة النصية عن طريق إعادة استخدام getBoardStr() الخاص بصنف TTTBoard. يعيّن تابع getBoardStr() قيمة المتغيرين xCanWin و oCanWin إلى False، وينسخ احتياطيًا القاموس self._spaces إلى المتغير originalSpaces (السطر ذو الرقم 2)، ثم تُنفَّذ حلقة for على كل أماكن اللوحة من 1 إلى 9. تُضبط سمة self._spaces لنسخ المكتبة originalSpaces، وإذا كانت الخلية فارغة تُوضع X مكانها، إذ يحفز هذا تحريك X إلى الفراغ التالي. سيحدد استدعاء self.isWinner() إذا كانت هذه هي الحركة الرابحة؛ فإذا كانت كذلك تصبح xCanWin هي True. تُكرر هذه الخطوات من أجل O لمعرفة ما إذا كان O يربح بالتحرك إلى هذا المكان (السطر ذو الرقم 3). يستخدم هذا التابع وحدة copy لنسخ القاموس في self._spaces لذا نضيف السطر التالي لأول ملف tictactoe.py. import copy نغير بعدها السطر في main() لنستنسخ كائن HintBoard بدلًا من TTTBoard: gameBoard = HintBoard() # Create a TTT board object. يعمل البرنامج كما كان عدا تغيير السطر الوحيد في main() وعندما ينفذ البرنامج سيكون الخرج على النحو التالي: Welcome to Tic-Tac-Toe! --snip-- X| | 1 2 3 -+-+- | |O 4 5 6 -+-+- | |X 7 8 9 X can win in one more move. What is O's move? (1-9) 5 X| | 1 2 3 -+-+- |O|O 4 5 6 -+-+- | |X 7 8 9 O can win in one more move. --snip-- The game is a tie! Thanks for playing! في نهاية التابع: إذا كانت قيمة xCanWin و oCanWin هي True، تُضاف رسالةٌ إضافية تشير إلى ذلك إلى السلسلة النصية boardStr، وأخيرًا تُعاد القيمة boardStr. لا يحتاج كل تابع معاد تعريفه لاستخدام super()؛ فإذا كان يعمل التابع -الذي يعيد التعريف- شيئًا مختلفًا تمامًا عن التابع المُعاد تعريفه في الصنف الأب، لا توجد حاجة لاستدعاء التابع المعاد تعريفه باستخدام super(). تفيد الدالة super() على نحوٍ خاص عندما يكون للصنف أكثر من تابع أب كما موضح في الفقرة "الوراثة المتعددة" لاحقًا. فضل التكون Composition على الوراثة الوراثةهي تقنية جيدة لإعادة استخدام الشيفرة، وقد تفكر باستخدامها فورًا في الأصناف الخاصة بك، ولكن ربما لا تريد دومًا أن يكون الأساس والأصناف الفرعية مرتبطة جدًا، فإنشاء مستويات متعددة من الوراثة لا يرتب الشيفرة الخاصة لك أكثر ما يضيف بيروقراطية. على الرغم من أنه بإمكانك استخدام الوراثة للأصناف ذات العلاقات " is a" (بمعنى آخر، عندما يكون الصنف الابن هو نوع من أنواع الصنف الأب)، من المفضل استخدام تقنية تدعى التكوّن composition للأصناف ذات العلاقات "لديه has a". التكوّن هو تقنية تصميم لضم الكائنات في الأصناف الخاصة بك بدلًا من توارث أصناف تلك الكائنات. هذا ما نفعله عندما نضيف خاصيّات إلى الأصناف الخاصة بنا. عند تصميم الأصناف الخاصة بك باستخدام الوراثة فضّل التكوّن على الوراثة، هذا ما كنا نفعله في كل الأمثلة الحالية والسابقة كما يلي: كائن WizCoin "لديه has a" كمية من النقود من أنواع galleon و sickle و knut. كائن TTTBoard "لديه has a" مصفوفة بتسع فراغات. كائن MiniBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات. كائن HintBoard "هو is a" كائن TTTBoard لذا "لديه has a" مصفوفة من تسعة فراغات. لنعد إلى صنف WizCoin الذي أنشأناه سابقًا. إذا أنشأنا صنف WizardCustomer لتمثل الزبائن في العالم السحري، يجب على هؤلاء الزبائن حمل كمية من المال، الذي نعبّر عنه بصنف WizCoin ولكن لا توجد علاقة "is a" بين الصنفين؛ فكائن WizardCustomer ليس من نوع كائن WizCoin. إذا استخدمنا الوراثة، سنحصل على شيفرة برمجية غير مُعتادة: import wizcoin 1 class WizardCustomer(wizcoin.WizCoin): def __init__(self, name): self.name = name super().__init__(0, 0, 0) wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.value()} knuts worth of money.') print(f'{wizard.name}\'s coins weigh {wizard.weightInGrams()} grams.') في هذا المثال، يرث WizardCustomer توابع الكائن WizCoin مثل value() و weightInGrams(). تقنيًا يمكن للصنف WizardCustomer الذي ورث من WizCoin أن ينجز بجميع المهام التي ينجزها WizardCustomer، والتي تضم كائن WizCoin، كما تفعل السمة، ولكن اسمَي التابعين wizard.value() و wizard.weightInGrams() مضللة؛ إذ يبدو أنها تُعيد قيمة ووزن الساحر بدلًا من قيمة ووزن نقود الساحر. إضافةً إلى ذلك، إذا أردنا لاحقًا إضافة تابع weightInGrams() لوزن الساحر، سيكون هذا الاسم مأخوذًا مسبقًا. من الأسهل أن يكون الكائن WizCoin سمةً لأن الزبون الساحر "لديه" كميةً من نقود الساحر. import wizcoin class WizardCustomer: def __init__(self, name): self.name = name 1 self.purse = wizcoin.WizCoin(0, 0, 0) wizard = WizardCustomer('Alice') print(f'{wizard.name} has {wizard.purse.value()} knuts worth of money.') print(f'{wizard.name}\'s coins weigh {wizard.purse.weightInGrams()} grams.') بدلًا من جعل الصنف WizardCutomer يرث التوابع من WizCoin، نعطي للصنف WizardCutomer سمة purse التي تحتوي كائن WizCoin. أي تغييرات لتوابع الصنف WizCoin عند استخدام التكوّن لن تغير توابع الصنف WizardCustomer. تمنحك هذه الطريقة مرونةً أكبر في تغيير التصميمات المستقبلية لكلا الصنفين وتؤدي إلى شيفرة سهلة الصيانة. مساوئ الوراثة السيئة الأساسية في الوراثة هي أنه أي تغيير مستقبلي يحصل على الأصناف الأب سترثه كل الأصناف الابن. في بعض الحالات هذا الربط الشديد هو ما تحتاجه ولكن في بعض الحالات لا يفي نموذج الوراثة بمتطلبات الشيفرة. مثلًا، لنقل أنه لدينا الأصناف Car و Motorcycle و LunarRover في برنامج محاكاة عربات، ستحتاج هذه الأصناف إلى توابع متماثلة، مثل startIgnition() و changeTire(). بدلًا من نسخ ولصق الشيفرة إلى كل صنف، يمكننا إنشاء صنف أب Vehicle ونجعل Car و Motorcycle و LunarRover يرثونها. الآن نريد إصلاح خطأ في تابع changeTire()، وسنجري التغيير في مكان واحد. هذا مفيد جدًا، إذ لدينا العديد من أصناف العربات التي ترث من Vehicle. ستكون شيفرة هذه الأصناف على النحو التالي: class Vehicle: def __init__(self): print('Vehicle created.') def startIgnition(self): pass # Ignition starting code goes here. def changeTire(self): pass # Tire changing code goes here. class Car(Vehicle): def __init__(self): print('Car created.') class Motorcycle(Vehicle): def __init__(self): print('Motorcycle created.') class LunarRover(Vehicle): def __init__(self): print('LunarRover created.') لكن كل التغييرات المستقبلية على Vehicle ستؤثر على هذه الأصناف الفرعية أيضًا. ماذا سيحدث لو أردنا تابع changeSparkPlug()؟ لدى السيارات والدراجات النارية محركات احتراق بشمعات احتراق ولكن العربات القمرية lunar rovers ليس لديها ذلك. يمكننا -بتفضيل التكوّن على الوراثة- إنشاء صنفي CombustionEngine و ElectricEngine، ثم تصميم صنف Vehicle ليكون "لديه has a" سمة محرك إما CombustionEngine أو ElectricEngine مع التوابع الموافقة. class CombustionEngine: def __init__(self): print('Combustion engine created.') def changeSparkPlug(self): pass # هنا الشيفرة البرمجية التي تعدّل على شمعة الاحتراق class ElectricEngine: def __init__(self): print('Electric engine created.') class Vehicle: def __init__(self): print('Vehicle created.') self.engine = CombustionEngine() # استخدم هذا المحرك افتراضيًا --snip-- class LunarRover(Vehicle): def __init__(self): print('LunarRover created.') self.engine = ElectricEngine() يتطلب هذا إعادة كتابة كمية كبيرة من الشيفرة خصوصًا إذا كان لدينا عدة أصناف ترث من الصنف Vehicle الموجود مسبقًا. كل استدعاءات vehicleObj.changeSparkPlug() ستكون بحاجة لتصبح vehicleObj.engine.changeSparkPlug() لكل كائن في الصنف Vehicle أو أصنافها الفرعية لأن كل تغيير كبير سيحدث أخطاءً ربما تجعل من التابع changeSparkPlug() الخاص بالصنف LunarVehicle لا يفعل شيئًا. تتمثل الطريقة الخاصة ببايثون في هذه الحالة بضبط قيمة changeSparkPlug إلى None في صنف LunarVehicle: class LunarRover(Vehicle): changeSparkPlug = None def __init__(self): print('LunarRover created.') يتبع السطر: changeSparkPlug = None الصياغة المعرفة في "سمات الصنف" التي سنناقشها لاحقًا، وهذا يعيد تعريف التابع changeSparkPlug() الموروث من Vehicle، لذا يسبب استدعاؤه باستخدام كائن LunarRover خطأ: >>> myVehicle = LunarRover() LunarRover created. >>> myVehicle.changeSparkPlug() Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'NoneType' object is not callable يؤدي هذا الخطأ إلى فشل البرنامج وتوقفه سريعًا، ويمكننا مباشرةً ملاحظة المشكلة عند استدعاء التابع غير المناسب باستخدام كائن LunarRover. يرث أيضًا كل صنف ابن للصنف LunarRover القيمة None للتابع changeSparkPlug(). تخبرنا رسالة الخطأ التالية بأن مبرمج الصنف LunarRover تعمّد ضبط قيمة التابع changSprakPlug() إلى None: TypeError: 'NoneType' object is not callable إذا لم يكن هناك بالأساس تابع، سنحصل على رسالة الخطأ التالية: NameError: name 'changeSparkPlug' is not defined تخلق الوراثة أصنافًا فيها تعقيدات وتناقضات لذا يُفضل استخدام التكوّن بدلًا عنها. الخلاصة الوراثة هي تقنية لإعادة استخدام الشيفرة، تسمح لك إنشاء أصناف ابن التي ترث توابع أصناف الأب، يمكنك إعادة تعريف التوابع لتقدم شيفرةً جديدةً لهم واستخدام super() لاستدعاء التابع الأصلي من الصنف الأب. لدى الأصناف الابن علاقة "is a" مع الصنف الأب الخاصة بها، لأن كائن من الصنف الابن هو كائن للصنف الأب. استخدام الأصناف والوراثة في بايثون اختياري، إذ يرى بعض المبرمجين أن التعقيد المرافق للاستخدام الكثير للوراثة لا يبرر فائدته. من المرونة أكثر استخدام التكوّن بدلًا من الوراثة لأنها تنفذ علاقة "has a" مع كائن من أحد الأصناف وكان من أصناف أخرى بدلًا من وراثة التوابع مباشرةً من هذه الأصناف، فمثلًا قد يحتوي كائن Customer على سمة birthday المسندة إلى كائن Date بدلًا من أن يكون هناك أصناف فرعية من صنف Customer للكائن Date. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Inheritance من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق مقارنة ما بين برامج بايثون Python الاعتيادية وبرامج بايثون كائنية التوجه البرمجة كائنية التوجه (Object Oriented Programming) في بايثون
-
تعرّفنا في المقال السابق على مفهوم البرمجة كائنية التوجه - أو اختصارًا OOP- وكيفية تعريف الأصناف classes في لغة بايثون، إضافةً إلى بعض التوابع المفيدة بهذا الخصوص. سننظر في هذا المقال على مثال عملي لتطبيق البرمجة كائنية التوجه في لغة بايثون Python ومن ثم سنطّلع على البرنامج ذاته دون استخدام البرمجة كائنية التوجه Non-OOP. أمثلة مع البرمجة كائنية التوجه وبدونها: لعبة إكس أو في البداية، قد يكون من الصعب معرفة كيفية استخدام الأصناف في برامجك. دعنا نلقي نظرةً على مثال قصير للعبة إكس أو لا يستخدم الأصناف، ثم نعيد كتابته باستخدامها. افتح نافذة محرر ملفات جديدة وأدخل البرنامج التالي؛ ثم احفظه بالاسم tictactoe.py: # tictactoe.py, A non-OOP tic-tac-toe game. ALL_SPACES = list('123456789') # The keys for a TTT board dictionary. X, O, BLANK = 'X', 'O', ' ' # Constants for string values. def main(): """Runs a game of tic-tac-toe.""" print('Welcome to tic-tac-toe!') gameBoard = getBlankBoard() # Create a TTT board dictionary. currentPlayer, nextPlayer = X, O # X goes first, O goes next. while True: print(getBoardStr(gameBoard)) # Display the board on the screen. # Keep asking the player until they enter a number 1-9: move = None while not isValidSpace(gameBoard, move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() updateBoard(gameBoard, move, currentPlayer) # Make the move. # Check if the game is over: if isWinner(gameBoard, currentPlayer): # First check for victory. print(getBoardStr(gameBoard)) print(currentPlayer + ' has won the game!') break elif isBoardFull(gameBoard): # Next check for a tie. print(getBoardStr(gameBoard)) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns. print('Thanks for playing!') def getBlankBoard(): """Create a new, blank tic-tac-toe board.""" board = {} # The board is represented as a Python dictionary. for space in ALL_SPACES: board[space] = BLANK # All spaces start as blank. return board def getBoardStr(board): """Return a text-representation of the board.""" return f''' {board['1']}|{board['2']}|{board['3']} 1 2 3 -+-+- {board['4']}|{board['5']}|{board['6']} 4 5 6 -+-+- {board['7']}|{board['8']}|{board['9']} 7 8 9''' def isValidSpace(board, space): """Returns True if the space on the board is a valid space number and the space is blank.""" return space in ALL_SPACES and board[space] == BLANK def isWinner(board, player): """Return True if player is a winner on this TTTBoard.""" b, p = board, player # Shorter names as "syntactic sugar". # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals. return ((b['1'] == b['2'] == b['3'] == p) or # Across the top (b['4'] == b['5'] == b['6'] == p) or # Across the middle (b['7'] == b['8'] == b['9'] == p) or # Across the bottom (b['1'] == b['4'] == b['7'] == p) or # Down the left (b['2'] == b['5'] == b['8'] == p) or # Down the middle (b['3'] == b['6'] == b['9'] == p) or # Down the right (b['3'] == b['5'] == b['7'] == p) or # Diagonal (b['1'] == b['5'] == b['9'] == p)) # Diagonal def isBoardFull(board): """Return True if every space on the board has been taken.""" for space in ALL_SPACES: if board[space] == BLANK: return False # If a single space is blank, return False. return True # No spaces are blank, so return True. def updateBoard(board, space, mark): """Sets the space on the board to mark.""" board[space] = mark if __name__ == '__main__': main() # Call main() if this module is run, but not when imported. عند تنفيذ هذا البرنامج، سيبدو الخرج كما يلي: Welcome to tic-tac-toe! | | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is X's move? (1-9) 1 X| | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is O's move? (1-9) --snip-- X| |O 1 2 3 -+-+- |O| 4 5 6 -+-+- X|O|X 7 8 9 What is X's move? (1-9) 4 X| |O 1 2 3 -+-+- X|O| 4 5 6 -+-+- X|O|X 7 8 9 X has won the game! Thanks for playing! باختصار، يعمل هذا البرنامج باستخدام كائنات القاموس لتُمثل المساحات التسع على لوحة إكس أو. مفاتيح القاموس هي السلاسل من "1" إلى "9"، وقيمها هي السلاسل "X" أو "O" أو " ". المسافات المرقمة بنفس ترتيب لوحة مفاتيح الهاتف. تتمثل وظيفة الدوال في tictactoe.py بما يلي: تحتوي الدالة main() على الشيفرة التي تُنشئ بنية بيانات لوحة جديدة (مخزنة في متغير gameBoard) وتستدعي دوالًا أخرى في البرنامج. تُعيد الدالة getBlankBoard() قاموسًا به تسع مسافات مضبوطة على " " للوحة فارغة. تقبل الدالة getBoardStr() قاموسًا يمثل اللوحة وتعيد تمثيلًا لسلسلة متعددة الأسطر للوحة يمكن طباعتها على الشاشة، وتصيّر هذه الدالة نص لوحة إكس أو tic-tac-toe الذي تعرضه اللعبة. تُعيد الدالة isValidSpace() القيمة True إذا مُرّر رقم مسافة صالح وكانت تلك المسافة فارغة. تقبل معاملات دالة isWinner() قاموس لوحة إما "X" أو "O" لتحديد ما إذا كان هذا اللاعب لديه ثلاث علامات متتالية على اللوحة. تحدد دالة isBoardFull() ما إذا كانت اللوحة لا تحتوي على مسافات فارغة، ما يعني أن اللعبة قد انتهت. تقبل معاملات دالة updateBoard() قاموس لوحة ومساحة وعلامة X أو O للاعب وتحدّث القاموس. لاحظ أن العديد من الدوال تقبل اللوحة المتغيرة في معاملها الأول، وهذا يعني أن هذه الدوال مرتبطة ببعضها بعضًا من حيث أنها تعمل جميعها على بنية بيانات مشتركة. عندما تعمل العديد من الدوال في الشيفرة على بنية البيانات ذاتها، فمن الأفضل عادةً تجميعها معًا على أنها توابع وسمات للصنف. دعنا نعيد تصميم هذا في برنامج tictactoe.py لاستخدام صنف TTTBoard الذي سيخزن قاموس board في سمة تسمى spaces. ستصبح الدوال التي كان لها board مثل معامل توابع لصنف TTTBoard الخاصة بنا وستستخدم المعامل self بدلًا من معامل board. افتح نافذة محرر ملفات جديدة، وأدخل الشيفرة التالي، واحفظه باسم tictactoe_oop.py: # tictactoe_oop.py, an object-oriented tic-tac-toe game. ALL_SPACES = list('123456789') # The keys for a TTT board. X, O, BLANK = 'X', 'O', ' ' # Constants for string values. def main(): """Runs a game of tic-tac-toe.""" print('Welcome to tic-tac-toe!') gameBoard = TTTBoard() # Create a TTT board object. currentPlayer, nextPlayer = X, O # X goes first, O goes next. while True: print(gameBoard.getBoardStr()) # Display the board on the screen. # Keep asking the player until they enter a number 1-9: move = None while not gameBoard.isValidSpace(move): print(f'What is {currentPlayer}\'s move? (1-9)') move = input() gameBoard.updateBoard(move, currentPlayer) # Make the move. # Check if the game is over: if gameBoard.isWinner(currentPlayer): # First check for victory. print(gameBoard.getBoardStr()) print(currentPlayer + ' has won the game!') break elif gameBoard.isBoardFull(): # Next check for a tie. print(gameBoard.getBoardStr()) print('The game is a tie!') break currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns. print('Thanks for playing!') class TTTBoard: def __init__(self, usePrettyBoard=False, useLogging=False): """Create a new, blank tic tac toe board.""" self._spaces = {} # The board is represented as a Python dictionary. for space in ALL_SPACES: self._spaces[space] = BLANK # All spaces start as blank. def getBoardStr(self): """Return a text-representation of the board.""" return f''' {self._spaces['1']}|{self._spaces['2']}|{self._spaces['3']} 1 2 3 -+-+- {self._spaces['4']}|{self._spaces['5']}|{self._spaces['6']} 4 5 6 -+-+- {self._spaces['7']}|{self._spaces['8']}|{self._spaces['9']} 7 8 9''' def isValidSpace(self, space): """Returns True if the space on the board is a valid space number and the space is blank.""" return space in ALL_SPACES and self._spaces[space] == BLANK def isWinner(self, player): """Return True if player is a winner on this TTTBoard.""" s, p = self._spaces, player # Shorter names as "syntactic sugar". # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals. return ((s['1'] == s['2'] == s['3'] == p) or # Across the top (s['4'] == s['5'] == s['6'] == p) or # Across the middle (s['7'] == s['8'] == s['9'] == p) or # Across the bottom (s['1'] == s['4'] == s['7'] == p) or # Down the left (s['2'] == s['5'] == s['8'] == p) or # Down the middle (s['3'] == s['6'] == s['9'] == p) or # Down the right (s['3'] == s['5'] == s['7'] == p) or # Diagonal (s['1'] == s['5'] == s['9'] == p)) # Diagonal def isBoardFull(self): """Return True if every space on the board has been taken.""" for space in ALL_SPACES: if self._spaces[space] == BLANK: return False # If a single space is blank, return False. return True # No spaces are blank, so return True. def updateBoard(self, space, player): """Sets the space on the board to player.""" self._spaces[space] = player if __name__ == '__main__': main() # Call main() if this module is run, but not when imported. يقدّم هذا البرنامج عمل برنامج إكس أو tic-tac-toe السابق ذاته دون استخدام البرمجة كائنية التوجه. يبدو الخرج متطابقًا تمامًا. نقلنا الشيفرة التي كانت في getBlankBoard() إلى تابع __init __() لصنف TTTBoard، لأنها تؤدي المهمة ذاتها لإعداد بنية بيانات اللوحة. حوّلنا الدوال الأخرى إلى توابع، مع استبدال المعامل board القديم بمعامل self، لأنها تخدم أيضًا غرضًا مشابهًا؛ إذ أن كلاهما كتلتان من الشيفرة البرمجية التي تعمل على بنية بيانات لوحة إكس أو. عندما تحتاج الشيفرة البرمجية في هذه التوابع إلى تغيير القاموس المخزن في السمة _spaces، تستخدم الشيفرة self._spaces، وعندما تحتاج الشيفرة في هذا التابع إلى استدعاء توابع أخرى، فإن الاستدعاء يسبقه self وفترة زمنية period. هذا مشابه لكيفية احتواء coinJars.values() في قسم "إنشاء صنف بسيط" على كائن في متغير coinJars. في هذا المثال، الكائن الذي يحتوي على طريقة استدعاء موجود في متغير self. لاحظ أيضًا أن السمة _spaces تبدأ بشرطة سفلية، ما يعني أن الشيفرة البرمجية الموجودة داخل توابع TTTBoard هي فقط التي يجب أن تصل إليها أو تعدلها. يجب أن تكون الشيفرة البرمجية خارج الصنف قادرةً فقط على تعديل المسافات بصورة غير مباشرة عن طريق استدعاء التوابع التي تعدّلها. قد يكون من المفيد مقارنة الشيفرة المصدرية لبرنامجي إكس أو، إذ يمكنك مقارنة الشيفرة وعرض مقارنة جنبًا إلى جنب من خلال الرابط https://autbor.com/compareoop. لعبة إكس أو هي برنامج صغير، لذا لا يتطلب فهمه الكثير من الجهد، ولكن ماذا لو كان هذا البرنامج يتكون من عشرات الآلاف من السطور بمئات الدوال المختلفة؟ قد يكون فهم البرنامج الذي يحتوي على بضع عشرات من الأصناف أسهل في الفهم من البرنامج الذي يحتوي على عدة مئات من الدوال المتباينة. تُقسّم البرمجة كائنية التوجه البرنامج المعقد إلى أجزاء يسهل فهمها. تصميم أصناف في العالم الحقيقي أمر صعب يبدو تصميم الصنف، تمامًا مثل تصميم الاستمارة الورقية paper form، فهو أمرٌ واضحٌ وبسيط. الاستمارات والأصناف، بحكم طبيعتها، هي تبسيطات لكائنات العالم الحقيقي التي تمثلها. السؤال هو كيف نبسط هذه الأشياء؟ على سبيل المثال، إذا كنا بصدد إنشاء صنف Customer فيجب أن يكون للعميل سمة firstName و lastName، أليس كذلك؟ لكن في الواقع، قد يكون إنشاء أصناف لنمذجة كائنات من العالم الحقيقي أمرًا صعبًا؛ ففي معظم البلدان الغربية، يكون الاسم الأخير للشخص هو اسم عائلته، ولكن في الصين، يكون اسم العائلة أولًا. إذا كنا لا نريد استبعاد أكثر من مليار عميل محتمل، فكيف يجب أن نغير صنف Customer لدينا؟ هل يجب تغيير firstName و lastName إلى givenName و familyName؟ لكن بعض الثقافات لا تستخدم أسماء العائلة. على سبيل المثال، الأمين العام السابق للأمم المتحدة يو ثانت U Thant، وهو بورمي، ليس له اسم عائلة: ثانت Thant هو اسمه الأول ويو U هو اختصار لاسم والده. قد نرغب في تسجيل عمر العميل، ولكن سرعان ما ستصبح سمة age قديمة؛ وبدلًا من ذلك، من الأفضل حساب العمر في كل مرة تحتاج إليها باستخدام سمة birthdate. العالم الحقيقي معقد، ومن الصعب تصميم الاستمارات والأصناف لتسجيل هذه الأمور المعقدة في بنية موحدة يمكن لبرامجنا العمل عليها؛ إذ تختلف تنسيقات أرقام الهاتف بين البلدان؛ ولا تنطبق الرموز البريدية على العناوين خارج الولايات المتحدة؛ كما قد يكون تعيين الحد الأقصى لعدد الأحرف لأسماء المدن مشكلةً بالنسبة إلى قرية SchmedeswMorewesterdeich الألمانية. في أستراليا ونيوزيلندا، يمكن أن يكون جنسك المعترف به قانونًا هو X. خلد الماء هو أحد الثدييات التي تبيض. لا ينتمي الفول السوداني للمكسرات. الهوت دوج قد تكون شطيرة أو قد لا تكون، اعتمادًا على من تسأل. بصفتك مبرمجًا يكتب برامج لاستخدامها في العالم الحقيقي، سيتعين عليك تجاوز هذا التعقيد. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Classes من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق البرمجة كائنية التوجه Object-Oriented Programming والأصناف Classes في لغة بايثون تعلم كتابة أكواد بايثون من خلال الأمثلة العملية كيف تكتب أول برنامج لك في بايثون
-
البرمجة كائنية التوجه - أو اختصارًا OOP- هي ميزة للغة البرمجة تسمح لك بجمع الدوال functions والمتغيرات variables معًا في أنواع بيانات data type جديدة، تسمى الأصناف classes، والتي يمكنك من خلالها إنشاء كائنات objects. يمكنك تقسيم البرنامج المترابط إلى أجزاء أصغر يسهل فهمها وتنقيح أخطائها عن طريق تنظيم الشيفرة البرمجية الخاصة بك إلى أصناف. لا تضيف البرمجة كائنية التوجه تنظيمًا بالنسبة للبرامج الصغيرة، بل تقّدم تعقيدًا لا داعٍ له في بعض الحالات، وعلى الرغم من أن بعض اللغات، مثل جافا، تتطلب منك تنظيم كل الشيفرات البرمجية في أصناف، إلا أن ميزات البرمجة كائنية التوجه في بايثون اختيارية، إذ يمكن للمبرمجين الاستفادة من الأصناف إذا كانوا بحاجة إليها أو تجاهلها. يشير حديث مُبرمج بايثون Jack Diederich في PyCon 2012، "توقف عن كتابة الأصناف"، إلى العديد من الحالات التي يستخدم فيها المبرمجون الأصناف عندما تفي دالة بسيطة بالغرض، ولكن بصفتك مبرمجًا، يجب أن تكون على دراية بأساسيات الأصناف وكيف تعمل، لذلك سنتعرف في هذا المقال على ماهية الأصناف، ولماذا تُستخدَم في البرامج، ومفاهيم البرمجة القائمة عليها. البرمجة كائنية التوجه موضوع واسع، وهذا المقال مجرّد مقدمة. تشبيه من العالم الحقيقي: تعبئة استمارة لعلّك ملأت الاستمارات الورقية والإلكترونية عدة مرات في حياتك؛ منها لزيارات الطبيب أو لعمليات الشراء عبر الإنترنت أو للرد على دعوة لحضور حفل زفاف. تُستخدم الاستمارة بمثابة طريقة رسمية من قبل شخص آخر أو منظمة أخرى لجمع المعلومات التي يحتاجونها عنك. تسأل الاستمارات المختلفة أنواعًا متنوعة من الأسئلة، فمثلًا يجب عليك شرح حالتك الطبية الحساسة في استمارة الطبيب، بينما عليك الإبلاغ عن أي ضيوف تحضرهم معك إلى حفل الزفاف في استمارة الحفل. في بايثون، يكون للصنف والنوع ونوع البيانات المعنى ذاته، ومثال عن ذلك النموذج الورقي أو الإلكتروني؛ فالصنف هو مخطط لكائنات بايثون (وتسمى أيضًا النُسَخ instances)، والتي تحتوي على البيانات الممثّلة لاسم، ويمكن أن يكون هذا الاسم هو مريض الطبيب، أو عملية شراء إلكترونية، أو ضيف حفل زفاف. تشبه الأصناف قالب استمارة فارغة، وتشبه الكائنات التي تُنشأ من ذلك الصنف الاستمارة المملوءة التي تحتوي على بيانات فعلية حول نوع الشيء الذي يمثله النموذج. على سبيل المثال، تُشبه استمارة استجابة دعوة حفل الزفاف RSVP الصنف، في حين تشبه دعوة حفل الزفاف RSVP المملوء الكائن في الشكل 1. [الشكل 1: تُشبه قوالب استمارة دعوة الزفاف الأصناف، في حين تُشبه الاستمارات المعبأة الكائنات] يمكنك أيضًا النظر إلى الأصناف والكائنات على أنها جداول بيانات، كما في الشكل 2. [الشكل 2: جدول بيانات لجميع بيانات دعوات حفل الزفاف] ستشكل ترويسات الأعمدة الأصناف، وتشكل الصفوف rows الفردية كائنًا. يأتي غالبًا ذكر الأصناف والكائنات على أنها نماذج بيانات للعناصر في العالم الحقيقي، ولكن لا تخلط بين الخريطة map والمنطقة؛ فما تحتويه الأصناف يعتمد على ما يحتاج البرنامج لفعله. يوضح الشكل 3 بعض الكائنات من أصناف مختلفة تمثل جميعها شخصًا، وتخزن معلومات مختلفة تمامًا باختلاف اسم الشخص. [الشكل 3: أربعة كائنات مصنوعة من أصناف مختلفة تمثل شخصًا، اعتمادًا على ما يحتاج التطبيق إلى معرفته عن الشخص] يجب أيضًا أن تعتمد المعلومات الموجودة في أصنافك على احتياجات برنامجك، إذ تستخدم العديد من برامج البرمجة كائنية التوجه التعليمية صنف Car مثالًا أساسيًا دون الإشارة إلى أن ما يوجد في الصنف يعتمد كليًا على نوع البرنامج الذي تكتبه. لا يوجد هناك ما يدعى صنف Car العام الذي من الواضح أنه يحتوي على تابع honkHorn() أو سمة numberOfCupholders لمجرد أنها خصائص تمتلكها سيارات العالم الحقيقي؛ فقد يكون برنامجك لتطبيق ويب لبيع السيارات أو للعبة فيديو لسباق السيارات أو لمحاكاة حركة المرور على الطرق؛ وقد يكون لصنف السيارات الخاصة بتطبيق الويب لبيع السيارات على الويب سمات milesPerGallon أو manufacturersSuggestedRetailPrice (تمامًا كما قد تستخدم جداول بيانات وكالة السيارات هذه كعمود)، لكن لن تحتوي لعبة الفيديو ومحاكاة حركة المرور على الطرق على هذه الأصناف، لأن هذه المعلومات ليست ذات صلة بهما. قد يحتوي صنف السيارات الخاصة بلعبة الفيديو على explodeWithLargeFireball()، ولكن لن يحتوي تطبيق المحاكاة أو بيع السيارات على الصنف ذاته. إنشاء كائنات من الأصناف سبق لك استخدام الأصناف والكائنات في بايثون، حتى لو لم تُنشئ الأصناف بنفسك. تذكّر وحدة datetime، التي تحتوي على صنف باسم date، إذ تُمثل كائنات صنف datetime.date (تسمى أيضًا ببساطة كائنات اdatetime.date أو كائنات date) تاريخًا محددًا. أدخل ما يلي في الصدفة التفاعلية Interactive Shell لإنشاء كائن من صنف datetime.date: >>> import datetime >>> birthday = datetime.date(1999, 10, 31) # مرّر قيمة السنة والشهر واليوم >>> birthday.year 1999 >>> birthday.month 10 >>> birthday.day 31 >>> birthday.weekday() # يمثّل weekday() تابعًا؛ لاحظ القوسين 6 السمات Attributes -أو يطلق عليها أحيانًا الخاصيات- هي متغيرات مرتبطة بالكائنات. يؤدي استدعاء datetime.date() إلى إنشاء كائن date جديد، جرت تهيئته باستخدام الوسطاء 1999, 10, 31، بحيث يمثل الكائن تاريخ 31 أكتوبر 1999. نعيّن هذه الوسطاء على أنها سمات للصنف date، وهي year و month و day، التي تحتوي على جميع كائنات date. يمكن -باستخدام هذه المعلومات- لتابع الصنف weekday() حساب يوم الأسبوع. في هذا المثال، تُعاد القيمة 6 ليوم الأحد، لأنه وفقًا لتوثيق بايثون عبر الإنترنت، القيمة المُعادة من weekday() هي عدد صحيح يبدأ من 0 ليوم الاثنين وينتهي بالعدد 6 ليوم الأحد. يسرد توثيق بايثون العديد من التوابع الأخرى التي تمتلكها كائنات صنف date. على الرغم من أن كائن date يحتوي على سمات وتوابع متعددة، لكنه لا يزال كائنًا واحدًا يمكنك تخزينه في متغير، مثل birthday في هذا المثال. إنشاء صنف بسيط- WizCion دعنا ننشئ صنف WizCoin الذي يمثل عددًا من العملات في عالم سحري خيالي. فئات هذه العملة هي: knuts، و sickles (بقيمة 29 knuts)، و galleons (بقيمة 17 sickles أو 493 knuts). ضع في حساباتك أن العناصر الموجودة في صنف WizCoin تمثل كميةً من العملات، وليس مبلغًا من المال. على سبيل المثال، ستُخبرك أنك تمتلك خمسة أرباع سنت وعشرة سنتات بدلًا من 1.35 دولار. في ملف جديد باسم wizcoin.py، ضِف الشيفرة التالي لإنشاء صنف WizCoin. لاحظ أن اسم دالة __init__ له شرطتان سفليتان قبل وبعد init (سنناقش __init__ في " التابعين __init__() والمعامل self" لاحقًا): 1 class WizCoin: 2 def __init__(self, galleons, sickles, knuts): """إنشاء كائن WizCoin جديد باستخدام galleons و sickles و knuts""" self.galleons = galleons self.sickles = sickles self.knuts = knuts # ملاحظة: لا يوجد لتوابع ()__init__ قيمة مُعادة إطلاقًا 3 def value(self): """حساب القيمة بفئة knuts في غرض WizCoin لكل العملات""" return (self.galleons * 17 * 29) + (self.sickles * 29) + (self.knuts) 4 def weightInGrams(self): """حساب وزن العملات بالجرام""" return (self.galleons * 31.103) + (self.sickles * 11.34) + (self.knuts * 5.0) يعرّف هذا البرنامج صنفًا جديدًا يدعى WizCoin باستخدام التعليمة الأولى class، ويؤدي إنشاء صنف إلى إنشاء نوع جديد من الكائنات، إذ أن استخدام عبارة class لتعريف صنف يشبه عبارات def التي تعرّف دوالًا جديدة. توجد تعريفات لثلاثة توابع داخل كتلة التعليمات البرمجية التي تلي تعليمة class، هي: __init __() (اختصارًا للتهيئة)، و value()، و weightInGrams(). لاحظ أن جميع التوابع لها معامل أوّل يدعى self الذي سنكتشفه في القسم التالي. تكون أسماء الوحدات، مثل wizcoin في ملف wizcoin.py بأحرف صغيرة عادةً، بينما تبدأ أسماء الأصناف، مثل WizCoin بحرف كبير، ولكن للأسف، لا تتبع بعض الأصناف في مكتبة بايثون هذا الاصطلاح مثل صنف date. للتدرب على إنشاء كائنات جديدة لصنف WizCoin، ضِف الشيفرة المصدرية التالية في نافذة محرر ملفات منفصلة واحفظ الملف باسم wcexample1.py في المجلد wizcoin.py: import wizcoin 1 purse = wizcoin.WizCoin(2, 5, 99) # تُمرّر الأعداد الصحيحة إلى التابع ()__init__ print(purse) print('G:', purse.galleons, 'S:', purse.sickles, 'K:', purse.knuts) print('Total value:', purse.value()) print('Weight:', purse.weightInGrams(), 'grams') print() 2 coinJar = wizcoin.WizCoin(13, 0, 0) # تُمرّر الأعداد الصحيحة إلى التابع ()__init__ print(coinJar) print('G:', coinJar.galleons, 'S:', coinJar.sickles, 'K:', coinJar.knuts) print('Total value:', coinJar.value()) print('Weight:', coinJar.weightInGrams(), 'grams') تُنشئ استدعاءات WizCoin() كائن WizCoin وتُنفّذ الشيفرة في دالة__init __(). نمرّر هنا ثلاثة أعداد صحيحة مثل وسطاء إلى WizCoin() ثم يُعاد توجيه تلك الوسطاء إلى معاملات __init __()، تُعيَّن الوسطاء إلى سمات كائنات self.galleons و self.sickles و self.knuts. يجب علينا استيراد wizcoin ووضع .wizcoin قبل اسم دالة WizCoin() تمامًا كما تتطلب دالة time.sleep() أن تستورد وحدة time ووضع .time قبل اسم الدالة أولًا. عند تنفيذ البرنامج، سيبدو الخرج كما يلي: <wizcoin.WizCoin object at 0x000002136F138080> G: 2 S: 5 K: 99 Total value: 1230 Weight: 613.906 grams <wizcoin.WizCoin object at 0x000002136F138128> G: 13 S: 0 K: 0 Total value: 6409 Weight: 404.339 grams إذا تلقيت رسالة خطأ، مثل: ModuleNotFoundError: No module named 'wizcoin' تحقق أن الملف يحمل اسم wizcoin.py وأنه موجود في المجلد wcexample1.py ذاته. لا تمتلك كائنات WizCoin توضيحات نصية مفيدة، لذلك تعرض طباعة purse و coinJar عنوان تخزين بين قوسين (ستتعلم كيفية تغيير لاحقًا). يمكننا استدعاء التابعين value() و weightInGrams() على كائنات WizCoin التي خصصناها لمتغيري purse و coinJar، كما يمكننا استدعاء تابع السلسلة lower() على كائن سلسلة نصية. تحسب هذه التوابع القيم بناءً على سمات كائنات galleons و sickles و knuts. يُفيد استخدام الأصناف classes والبرمجة كائنية التوجه في إنتاج شيفرات برمجية أكثر قابلية للصيانة؛ أي شيفرة يسهل قراءتها وتعديلها وتوسيعها مستقبلًا. التابع __init __()والمعامل self التوابع هي دوال مرتبطة بكائنات من صنف معين. تذكر أن low() هو تابع لسلسلة، ما يعني أن استدعائه يكون على كائنات سلسلة نصية string. يمكنك استدعاء lower() من سلسلة، مثل 'Hello'.lower() ولكن لا يمكنك استدعائها على قائمة مثل: ['dog', 'cat'].lower(). لاحظ أيضًا أن التوابع تأتي بعد الكائن، والشيفرة الصحيحة هي 'Hello'.lower()، وليست lower('Hello'). على عكس تابع مثل lower()، لا ترتبط دالة مثل len() بنوع بيانات واحد؛ إذ يمكنك تمرير سلاسل وقوائم وقواميس وأنواع أخرى كثيرة من الكائنات إلى الدالة len(). نُنشئ كائنات عن طريق استدعاء اسم الصنف مثل دالة كما رأيت سابقًا، ويُشار إلى هذه الدالة على أنها دالة بانية constructor (أو باني، أو تُختصر باسم ctor، وتُنطق "see-tore") لأنها تُنشئ كائنًا جديدًا. نقول أيضًا أن الباني يبني نسخةً جديدةً للصنف. يؤدي استدعاء الباني إلى إنشاء كائن جديد ثم تنفيذ تابع __init __()، ولا يُطلب من الأصناف أن يكون لديها تابع __init __()، لكنها تملك هذا دائمًا تقريبًا. تابع __init __() هو المكان الذي تُعيّن فيه القيم الأولية للسمات عادةً. على سبيل المثال، تذكر أن تابع __init __() الخاص بالصنف WizCoin يبدو كما يلي: def __init__(self, galleons, sickles, knuts): """إنشاء كائن WizCoin جديد باستخدام galleons و sickles و knuts""" self.galleons = galleons self.sickles = sickles self.knuts = knuts # ملاحظة: لا يوجد لتوابع ()__init__ قيمة مُعادة إطلاقًا عندما يستدعي برنامج wcexample1.py ما يلي: WizCoin (2, 5, 99)، يبني بايثون كائن WizCoin جديد، ثم يمرر ثلاثة وسطاء (2 و 5 و 99) إلى استدعاء __init __()، لكن للتابع __init __() أربعة معاملات، هي: self و galleons و sickles و knuts، والسبب هو أن جميع التوابع لها معامل أول يدعى self. عندما يُستدعى تابع ما على كائن، يُمرّر الكائن تلقائيًا لمعامل self، وتُعيَّن بقية الوسطاء للمعاملات بصورة طبيعية. إذا رأيت رسالة خطأ، مثل: TypeError: __init__() takes 3 positional arguments but 4 were given ربما تكون قد نسيت إضافة معامل self إلى تعليمة def الخاصة بالتابع. لا يتعين عليك تسمية المعامل الأول للتابع بالاسم self، إذ يمكنك تسميته بأي شيء آخر، لكن استخدام self أمر تقليدي، واختيار اسم مختلف سيجعل الشيفرة الخاصة بك أقل قابلية للقراءة لمبرمجي بايثون الآخرين. عندما تقرأ الشيفرة، فإن وجود self مثل معامل أول هو أسرع طريقة يمكنك من خلالها تمييز التوابع عن الدوال، وبالمثل، إذا كانت شفرة تابعك لا تحتاج أبدًا إلى استخدام معامل self، فهذه علامة على أن تابعك يجب أن يكون مجرد دالة. لا تُعيَّن الوسطاء 2 و 5 و 99 في WizCoin (2, 5, 99) تلقائيًا إلى سمات الكائن الجديد؛ إذ نحتاج إلى عبارات الإسناد الثلاث في __init __() لإجراء ذلك. تُسمّى معاملات __init __() غالبًا باسم السمات ذاته، لكن يشير وجود self في self.galleons إلى أنها سمة من سمات الكائن، بينما يُعد galleons معاملًا. يعد تخزين وسطاء الباني في سمات الكائن مهمةً شائعةً لتابع __init __() للأصناف. نفّذ استدعاء datetime.date() في القسم السابق مهمةً مماثلةً باستثناء أن الوسطاء الثلاثة التي مررناها كانت لسمات year و month و day لكائن date الذي أُنشئ حديثًا. لقد سبق لك أن استدعيت الدوال int() و str() و float() و bool() للتحويل بين أنواع البيانات، مثل str (3.1415) للحصول على قيمة السلسلة '3.1415' بناءً على القيمة العشرية 3.1415. وصفنا ما سبق عندها على أنها دوال، لكن int و str و float و bool في الواقع أصناف، والدوال int() و str() و float() و bool() هي دوال بانية تعيد عددًا صحيحًا جديدًا أو سلسلة أو عدد عشري أو كائنات منطقية. يوصي دليل أسلوب بايثون باستخدام أحرف كبيرة لأسماء أصنافك، مثل WizCoin، على الرغم من أن العديد من أصناف بايثون المضمنة لا تتبع هذا الاصطلاح. يعيد استدعاء دالة الإنشاء WizCoin() الكائن WizCoin الجديد، لكن التابع __init __() لا يحتوي أبدًا على عبارة return بقيمة مُعادة. تؤدي إضافة قيمة إعادة إلى حدوث هذا الخطأ: TypeError: __init__() should return None. السمات السمات attributes -أو الخاصيات- هي متغيرات مرتبطة بكائن، ويصف توثيق بايثون السمات بأنها "أي اسم يتبع النقطة" على سبيل المثال، لاحظ تعبير birthday.year في القسم السابق، السمة year هي اسم يتبع النقطة. يمتلك كل كائن مجموعة السمات الخاصة به، فعندما أنشأ برنامج wcexample1.py كائنين WizCoin وخزّنهما في متغيرات purse و coinJar كان لسماتهما قيم مختلفة. يمكنك الوصول إلى هذه السمات وتعيينها تمامًا مثل أي متغير. للتدرب على إعداد السمات: افتح نافذة محرر ملفات جديدة وأدخل الشيفرة التالية، واحفظها بالاسم wcexample2.py في مجلد الملف wizcoin.py ذاته: import wizcoin change = wizcoin.WizCoin(9, 7, 20) print(change.sickles) # تطبع 7 change.sickles += 10 print(change.sickles) # تطبع 17 pile = wizcoin.WizCoin(2, 3, 31) print(pile.sickles) # تطبع 3 pile.someNewAttribute = 'a new attr' # إنشاء سمة جديدة print(pile.someNewAttribute) عند تنفيذ هذا البرنامج، يبدو الخرج كما يلي: 7 17 3 a new attr يمكنك التفكير في سمات الكائن بطريقة مشابهة لمفاتيح القاموس، إذ يمكنك قراءة وتعديل القيم المرتبطة بها وتعيين سمات جديدة للكائن، تقنيًا تُعدّ التوابع سمات للأصناف أيضًا. السمات والتوابع الخاصة يمكن تمييز السمات على أنها تتمتع بوصول خاص في لغات مثل C++ أو جافا، ما يعني أن المصرِّف compiler أو المُفسر interpreter يسمح فقط للشيفرة الموجودة في توابع الأصناف بالوصول إلى سمات كائنات تلك الصنف فقط أو تعديلها، لكن هذا الأمر غير موجود في بايثون، إذ تمتلك جميع السمات والتوابع وصولًا عامًا public access فعال، ويمكن للشيفرة خارج الصنف الوصول إلى أي سمة وتعديلها في أي كائن من ذلك الصنف. الوصول الخاص مفيد، إذ يمكن مثلًا أن تحتوي كائنات صنف BankAccount على سمة balance التي لا يجب الوصول إليها إلا لتوابع صنف BankAccount. لهذه الأسباب، ينص اصطلاح بايثون على بدء أسماء السمات أو التوابع الخاصة بشرطة سفلية واحدة. تقنيًا، لا يوجد ما يمنع الشيفرة خارج الصنف من الوصول إلى السمات والتوابع الخاصة، ولكن من الممارسات المُثلى تُملي بالسماح لتوابع الصنف فقط بالوصول إليها. افتح نافذة محرر ملفات جديدة، وأدخل الشيفرة التالية، واحفظها باسم privateExample.py. تحتوي كائنات صنف BankAccount في هذه الشيفرة على السمتين _name و _balance الخاصتين والتي يمكن فقط لتابعَي deposit() و withdraw() الوصول إليهما مباشرةً: class BankAccount: def __init__(self, accountHolder): # يمكن لتوابع BankAccount الوصول إلى self._balance ولكن الشيفرة خارج هذا الصنف لا يمكنها الوصول 1 self._balance = 0 2 self._name = accountHolder with open(self._name + 'Ledger.txt', 'w') as ledgerFile: ledgerFile.write('Balance is 0\n') def deposit(self, amount): 3 if amount <= 0: return # لا تسمح بقيم سالبة self._balance += amount 4 with open(self._name + 'Ledger.txt', 'a') as ledgerFile: ledgerFile.write('Deposit ' + str(amount) + '\n') ledgerFile.write('Balance is ' + str(self._balance) + '\n') def withdraw(self, amount): 5 if self._balance < amount or amount < 0: return # لا يوجد نقود كافية في الحساب أو أن الرصيد سالب self._balance -= amount 6 with open(self._name + 'Ledger.txt', 'a') as ledgerFile: ledgerFile.write('Withdraw ' + str(amount) + '\n') ledgerFile.write('Balance is ' + str(self._balance) + '\n') acct = BankAccount('Alice') # أنشأنا حساب خاص بأليس acct.deposit(120) # يمكن تعديل السمة _balance باستخدام deposit() acct.withdraw(40) # يمكن تعديل السمة _balance باستخدام withdraw() # التغيير من _name و _balance أمر غير محبّذ ولكنه ممكن 7 acct._balance = 1000000000 acct.withdraw(1000) 8 acct._name = 'Bob' # نستطيع الآن التعديل على سجل Bob! acct.withdraw(1000) # عملية السحب هذه مسجلة في BobLedger.txt! عند تنفيذ privateExample.py، تكون الملفات التي تُنشأ غير دقيقة لأننا عدّلنا على _balance و _name خارج الصنف، مما أدى إلى حالات غير صالحة. يحتوي AliceLedger.txt على الكثير من المال بداخله: Balance is 0 Deposit 120 Balance is 120 Withdraw 40 Balance is 80 Withdraw 1000 Balance is 999999000 يوجد الآن ملف BobLedger.txt برصيد حساب لا يمكن تفسيره، على الرغم من أننا لم ننشئ كائن BankAccount لسجل Bob إطلاقًا: Withdraw 1000 Balance is 999998000 تكون الأصناف المصممة جيدًا في الغالب قائمة بحد ذاتها self-contained، مما يوفر توابع لضبط السمات على القيم الصحيحة. تُميَّز السمتين _balance و _name برقمي السطرين 1 و2، والطريقة الصالحة الوحيدة لتعديل قيمة صنف BankAccount هي من خلال التابعين deposit() و withdraw()؛ إذ يحقق هذان التابعان من تعليمة (3) وتعليمة (5) للتأكد من أن _balance لم توضع في حالة غير صالحة (مثل قيمة عدد صحيح سالب). يسجل هذان التابعان أيضًا كل معاملة لحساب الرصيد الحالي في تعليمة (4) وتعليمة (6). يمكن أن تضع الشيفرة البرمجية التي تعدل هذه السمات وتقع خارج الصنف، مثل تعليمة acct._balance = 1000000000 (التعليمة 7) أو تعليمة acct._name = 'Bob' (التعليمة ? ذلك الكائن في حالة غير صالحة ويتسبب بأخطاء وعمليات تدقيق من فاحص البنك. يصبح تصحيح الأخطاء أسهل باتباع اصطلاح بادئة الشرطة السفلية للوصول الخاص، والسبب هو أنك تعرف أن سبب الخطأ سيكون داخل شيفرة الصنف بدلًا من أي مكان في البرنامج بأكمله. لاحظ أنه على عكس جافا واللغات الأخرى، لا تحتاج بايثون إلى توابع getter و setter العامة للسمات الخاصة، وتستخدم بدلًا من ذلك الخاصيات properties، كما هو موضح لاحقًا. دالة type() وسمة qualname يخبرنا تمرير كائن إلى دالة type() المضمنة بنوع بيانات الكائن من خلال قيمته المُعادة، والكائنات التي تُعاد من دالة type() هي أنواع كائنات، وتسمى أيضًا كائنات الصنف. تذكر أن مصطلح النوع ونوع البيانات والصنف لها المعنى ذاته في بايثون. لمعرفة ما تُعيده دالة type() للقيم المختلفة، أدخل ما يلي في الصدفة التفاعلية: >>> type(42) # The object 42 has a type of int. <class 'int'> >>> int # int is a type object for the integer data type. <class 'int'> >>> type(42) == int # Type check 42 to see if it is an integer. True >>> type('Hello') == int # Type check 'Hello' against int. False >>> import wizcoin >>> type(42) == wizcoin.WizCoin # Type check 42 against WizCoin. False >>> purse = wizcoin.WizCoin(2, 5, 10) >>> type(purse) == wizcoin.WizCoin # Type check purse against WizCoin. True لاحظ أن int هو نوع كائن وهو نفس نوع الكائن الذي يُعيده type(42)، ولكن يمكن أيضًا تسميته بدالة بانية int()؛ إذ لا تحوّل الدالة int ('42') وسيط السلسلة '42'، وتُعيد بدلًا من ذلك كائن عدد صحيح بناءً على المعطيات. لنفترض أنك بحاجة إلى تسجيل بعض المعلومات حول المتغيرات في برنامجك لمساعدتك على تصحيحها لاحقًا. يمكنك فقط كتابة سلاسل إلى ملف السجل، ولكن تمرير كائن النوع إلى str() سيعيد سلسلة تبدو فوضوية إلى حد ما. بدلًا من ذلك، استخدم السمة __qualname__، التي تمتلكها جميع أنواع الكائنات، لكتابة سلسلة أبسط يمكن للبشر قراءتها: >>> str(type(42)) # Passing the type object to str() returns a messy string. "<class 'int'>" >>> type(42).__qualname__ # The __qualname__ attribute is nicer looking. 'int' تُستخدم سمة __qualname__ غالبًا لتجاوز تابع __repr __()، والتي سنشرحها بمزيد من التفصيل لاحقًا. الخلاصة البرمجة كائنية التوجه هي ميزة مفيدة لتنظيم الشيفرة البرمجية الخاصة بك. تتيح لك الأصناف تجميع البيانات والشيفرات البرمجية معًا في أنواع بيانات جديدة. يمكنك أيضًا إنشاء كائنات من هذه الأصناف عن طريق استدعاء بانيها (اسم الصنف المُستدعى مثل دالة)، والتي بدورها تستدعي تابع __init __() الخاص بالصنف. التوابع هي دوال مرتبطة بالكائنات، والسمات هي متغيرات مرتبطة بالكائنات. تحتوي جميع التوابع على معامل أول self، والذي يُعيّن للكائن عند استدعاء التابع. يسمح هذا للتوابع بقراءة سمات الكائن أو تعيينها واستدعاء توابعها. على الرغم من أن بايثون لا تسمح لك بتحديد الوصول الخاص أو العام للسمات، إلا أنها تمتلك اصطلاحًا باستخدام بادئة شرطة سفلية لأي تابع أو سمات يجب استدعاؤها أو الوصول إليها فقط من توابع الصنف الخاصة. يمكنك -باتباع هذه الاتفاقية- تجنب إساءة استخدام الصنف ووضعها في حالة غير صالحة يمكن أن تسبب أخطاء. سيعيد استدعاء type(obj) كائن صنف النوع obj. تحتوي كائنات الصنف على سمة __qualname___ التي تحتوي على سلسلة بشكل يمكن للبشر قراءته من اسم الصنف. في هذه المرحلة، ربما تفكر، لماذا يجب أن نهتم باستخدام الأصناف والسمات والتوابع بينما يمكننا إنجاز المهمة ذاتها مع الدوال؟ تُعد البرمجة كائنية التوجه طريقةً مفيدةً لتنظيم الشيفرات البرمجية الخاصة بك في أكثر من مجرد ملف ".py" يحتوي على 100 دالة فيه. من خلال تقسيم البرنامج إلى عدة أصناف مصممة جيدًا، يمكنك التركيز على كل صنف على حدة. البرمجة كائنية التوجه هي نهج يركز على هياكل البيانات وطرق التعامل مع هياكل البيانات تلك. هذا النهج ليس إلزاميًا لكل برنامج، ومن الممكن بالتأكيد الإفراط في استخدام البرمجة كائنية التوجه، لكن البرمجة كائنية التوجه توفر فرصًا لاستخدام العديد من الميزات المتقدمة التي سنستكشفها في الفصلين التاليين. أول هذه الميزات هو الوراثة inheritance التي سنتعمق فيها في الفصل التالي. ترجمة -وبتصرف- لقسم من الفصل Object-Oriented Programming And Classes من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: برمجة لعبة أربع نقاط في صف واحد Four-in-a-Row باستخدام لغة بايثون مصطلحات شائعة مثيرة للالتباس في بايثون. البرمجة كائنية التوجه كيفية إنشاء الأصناف وتعريف الكائنات في بايثون 3.
-
لعبة أربع نقاط في صف واحد Four-in-a-Row هي لعبة للاعبين اثنين، إذ يضع كل منهما حجرًا، ويحاول كل لاعب إنشاء صف مكون من أربعة من حجراته، سواء أفقيًا أو رأسيًا أو قطريًا، وهي مشابهة للعبتَين Connect Four و Four Up. تستخدم اللعبة لوحة قياس 7×6، وتشغل المربعات أدنى مساحة شاغرة في العمود. في لعبتنا، سيلعب لاعبان بشريان، X و O، ضد بعضهما، وليس لاعب بشري واحد ضد الحاسوب. خرج اللعبة سيبدو الخرج كما يلي عند تنفيذ برنامج أربع نقاط في صف واحد: Four-in-a-Row, by Al Sweigart al@inventwithpython.com Two players take turns dropping tiles into one of seven columns, trying to make four in a row horizontally, vertically, or diagonally. 1234567 +-------+ |.......| |.......| |.......| |.......| |.......| |.......| +-------+ Player X, enter 1 to 7 or QUIT: > 1 1234567 +-------+ |.......| |.......| |.......| |.......| |.......| |X......| +-------+ Player O, enter 1 to 7 or QUIT: --snip-- Player O, enter 1 to 7 or QUIT: > 4 1234567 +-------+ |.......| |.......| |...O...| |X.OO...| |X.XO...| |XOXO..X| +-------+ Player O has won! حاول اكتشاف العديد من الاستراتيجيات الدقيقة التي يمكنك استخدامها للحصول على أربعة أحجار متتالية بينما تمنع خصمك من فعل الشيء نفسه. الشيفرة المصدرية افتح ملفًا جديدًا في المحرر أو البيئة التطويرية IDE، وأدخل الشيفرة التالية، واحفظ الملف باسم "fourinarow.py": """Four-in-a-Row, by Al Sweigart al@inventwithpython.com A tile-dropping game to get four-in-a-row, similar to Connect Four.""" import sys # الثوابت المستخدمة لعرض اللوحة EMPTY_SPACE = "." # النقطة أسهل للعدّ والرؤية من المسافة PLAYER_X = "X" PLAYER_O = "O" # ملاحظة: عدّل قيمتي BOARD_TEMPLATE و COLUMN_LAVELS إذا تغيّر BOARD_WIDTH BOARD_WIDTH = 7 BOARD_HEIGHT = 6 COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7") assert len(COLUMN_LABELS) == BOARD_WIDTH # قالب السلسلة النصية الذي يُستخدم لطباعة اللوحة BOARD_TEMPLATE = """ 1234567 +-------+ |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| +-------+""" def main(): """Runs a single game of Four-in-a-Row.""" print( """Four-in-a-Row, by Al Sweigart al@inventwithpython.com Two players take turns dropping tiles into one of seven columns, trying to make Four-in-a-Row horizontally, vertically, or diagonally. """ ) # إعداد لعبة جديدة gameBoard = getNewBoard() playerTurn = PLAYER_X while True: # بدء دور اللاعب # عرض اللوحة قبل الحصول على حركة اللاعب displayBoard(gameBoard) playerMove = getPlayerMove(playerTurn, gameBoard) gameBoard[playerMove] = playerTurn # فحص حالة الفوز أو التعادل if isWinner(playerTurn, gameBoard): displayBoard(gameBoard) # عرض اللوحة لمرة أخيرة print("Player {} has won!".format(playerTurn)) sys.exit() elif isFull(gameBoard): displayBoard(gameBoard) # عرض اللوحة لمرة أخيرة print("There is a tie!") sys.exit() # تبديل الدور للاعب الآخر if playerTurn == PLAYER_X: playerTurn = PLAYER_O elif playerTurn == PLAYER_O: playerTurn = PLAYER_X def getNewBoard(): """Returns a dictionary that represents a Four-in-a-Row board. The keys are (columnIndex, rowIndex) tuples of two integers, and the values are one of the "X", "O" or "." (empty space) strings.""" board = {} for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): board[(columnIndex, rowIndex)] = EMPTY_SPACE return board def displayBoard(board): """Display the board and its tiles on the screen.""" # تحضير قائمة لتمريرها إلى تابع format() لقالب اللوحة # تحتوي القائمة على خلايا اللوحة بما في ذلك المسافات الفارغة # من اليسار إلى اليمين ومن الأعلى للأسفل tileChars = [] for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): tileChars.append(board[(columnIndex, rowIndex)]) # عرض اللوحة print(BOARD_TEMPLATE.format(*tileChars)) def getPlayerMove(playerTile, board): """Let a player select a column on the board to drop a tile into. Returns a tuple of the (column, row) that the tile falls into.""" while True: # استمر بسؤال اللاعب إلى أن يُدخل حركة صالحة print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:") response = input("> ").upper().strip() if response == "QUIT": print("Thanks for playing!") sys.exit() if response not in COLUMN_LABELS: print(f"Enter a number from 1 to {BOARD_WIDTH}.") continue # اطلب حركة من اللاعب مجددًا columnIndex = int(response) - 1 # نطرح واحد للحصول على فهرس يبدأ من الصفر # إذا كان العمود مليئًا، نطلب من اللاعب حركة مجددًا if board[(columnIndex, 0)] != EMPTY_SPACE: print("That column is full, select another one.") continue # اطلب حركة من اللاعب مجددًا # البدء من الأسفل واختيار أول خلية فارغة for rowIndex in range(BOARD_HEIGHT - 1, -1, -1): if board[(columnIndex, rowIndex)] == EMPTY_SPACE: return (columnIndex, rowIndex) def isFull(board): """Returns True if the `board` has no empty spaces, otherwise returns False.""" for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): if board[(columnIndex, rowIndex)] == EMPTY_SPACE: return False # أعد False إذا عُثر على مسافة فارغة return True # في حال كانت جميع الخلايا ممتلئة def isWinner(playerTile, board): """Returns True if `playerTile` has four tiles in a row on `board`, otherwise returns False.""" # تفقّد اللوحة بكاملها بحثًا عن حالة فوز for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT): # التحقق من حالة الفوز بالذهاب لليمين tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex)] tile3 = board[(columnIndex + 2, rowIndex)] tile4 = board[(columnIndex + 3, rowIndex)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True for columnIndex in range(BOARD_WIDTH): for rowIndex in range(BOARD_HEIGHT - 3): # التحقق من حالة فوز بالذهاب للأسفل tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex, rowIndex + 1)] tile3 = board[(columnIndex, rowIndex + 2)] tile4 = board[(columnIndex, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT - 3): # التحقق من حالة فوز بالذهاب قطريًا إلى اليمين والأسفل tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex + 1)] tile3 = board[(columnIndex + 2, rowIndex + 2)] tile4 = board[(columnIndex + 3, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True # التحقق من حالة فوز بالذهاب قطريًا إلى اليسار والأسفل tile1 = board[(columnIndex + 3, rowIndex)] tile2 = board[(columnIndex + 2, rowIndex + 1)] tile3 = board[(columnIndex + 1, rowIndex + 2)] tile4 = board[(columnIndex, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True return False # شغّل اللعبة إذا نُفّذ البرنامج بدلًا من استيراده if __name__ == "__main__": main() شغّل البرنامج السابق والعب بعض الجولات للحصول على فكرة عما يفعله هذا البرنامج قبل قراءة شرح الشيفرة المصدرية. للتحقق من وجود أخطاء كتابية، انسخها والصقها في أداة لكشف الاختلاف عبر الإنترنت. كتابة الشيفرة لنلقي نظرةً على الشيفرة المصدرية للبرنامج، كما فعلنا مع برنامج برج هانوي سابقًا. نسّقنا مرةً أخرى الشيفرة المصدرية باستخدام منسّق السطور Black بحد 75 محرفًا للسطر. نبدأ من الجزء العلوي للبرنامج: """Four-in-a-Row, by Al Sweigart al@inventwithpython.com A tile-dropping game to get four-in-a-row, similar to Connect Four.""" import sys # Constants used for displaying the board: EMPTY_SPACE = "." # A period is easier to count than a space. PLAYER_X = "X" PLAYER_O = "O" نبدأ البرنامج بسلسلة توثيق نصية docstring واستيراد للوحدات module، وتعيين للثوابت. كما فعلنا في برنامج برج هانوي. نعرّف الثابتَين PLAYER_X و PLAYER_O بحيث نبتعد عن استخدام سلاسل "X" و "O" ضمن البرنامج، مما يسهل اكتشاف الأخطاء. على سبيل المثال، سنحصل على استثناء NameError إذا أخطأنا بكتابة اسم الثابت، مثل كتابة PLAYER_XX مما يشير فورًا إلى المشكلة، ولكن إذا ارتكبنا خطأً كتابيًا باستخدام الحرف "X"، مثل "XX" أو "Z"، فقد لا يكون الخطأ الناتج واضحًا فورًا. كما هو موضح في قسم "الأرقام السحرية" من مقال اكتشاف دلالات الأخطاء في شيفرات لغة بايثون، فإن استخدام الثوابت بدلًا من قيمة السلسلة لا يمثل الوصف فحسب، بل يوفر أيضًا تحذير مبكر لأي أخطاء كتابية في الشيفرة المصدرية. ينبغي ألا تتغير الثوابت أثناء تشغيل البرنامج، لكن يمكن للمبرمج تحديث قيمهم في الإصدارات المستقبلية من البرنامج. لهذا السبب، نقدم ملاحظةً تخبر المبرمجين بضرورة تحديث ثابتي BOARD_TEMPLATE و COLUMN_LABELS، إذا غيروا قيمة BOARD_WIDTH: # Note: Update BOARD_TEMPLATE & COLUMN_LABELS if BOARD_WIDTH is changed. BOARD_WIDTH = 7 BOARD_HEIGHT = 6 بعد ذلك، ننشئ ثابت COLUMN_LABELS: COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7") assert len(COLUMN_LABELS) == BOARD_WIDTH سنستخدم هذا الثابت لاحقًا للتأكد من أن اللاعب يختار عمودًا صالحًا. لاحظ أنه في حالة تعيين BOARD_WIDTH على أي قيمة أخرى بخلاف 7، فسنضطر إلى إضافة تسميات labels إلى مجموعة tuple تدعى COLUMN_LABELS أو إزالتها منها. كان بإمكاننا تجنب ذلك من خلال إنشاء قيمة COLUMN_LABELS بناءً على BOARD_WIDTH بشيفرة مثل هذه: COLUMN_LABELS = tuple ([str (n) for n in range (1، BOARD_WIDTH + 1)]) لكن من غير المرجح أن يتغير COLUMN_LABELS في المستقبل، لأن لعبة أربع نقاط في صف واحد تربح تُلعب على لوحة 7×6، لذلك قررنا كتابة قيمة صريحة للمجموعة. بالتأكيد، تمثّل هذه الشيفرة شيفرة ذات رائحة smell code (وهي نمط شيفرة يشير إلى أخطاء محتملة)، ولكنها أكثر قابلية للقراءة من بديلها. تحذرنا تعليمة assert من تغيير BOARD_WIDTH بدون تحديث COLUMN_LABELS. كما هو الحال مع برج هانوي، يستخدم برنامج أربع في صف واحد تربح محارف آسكي ASCII لرسم لوحة اللعبة. تمثّل الأسطر التالية تعليمة إسناد واحدة بسلسلة نصية متعددة الأسطر: # The template string for displaying the board: BOARD_TEMPLATE = """ 1234567 +-------+ |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| |{}{}{}{}{}{}{}| +-------+""" تحتوي هذه السلسلة على أقواس معقوصة braces {} يحل محلها سلسلة باستخدام التابع format(). (ستعمل دالة displayBoard()، التي ستُشرح لاحقًا، على تحقيق هذا.) نظرًا لأن اللوحة تتكون من سبعة أعمدة وستة صفوف، فإننا نستخدم سبعة أزواج من القوسين {} في كل من الصفوف الستة لتمثيل كل فتحة. لاحظ أنه تمامًا مثل COLUMN_LABELS، فإننا نشفّر من الناحية الفنية اللوحة لإنشاء عدد محدد من الأعمدة والصفوف. إذا غيرنا BOARD_WIDTH أو BOARD_HEIGHT إلى أعداد صحيحة جديدة، فسنضطر أيضًا إلى تحديث السلسلة متعددة الأسطر في BOARD_TEMPLATE. كان بإمكاننا كتابة شيفرة لإنشاء BOARD_TEMPLATE استنادًا إلى الثابتين BOARD_WIDTH و BOARD_HEIGHT، مثل: BOARD_EDGE = " +" + ("-" * BOARD_WIDTH) + "+" BOARD_ROW = " |" + ("{}" * BOARD_WIDTH) + "|\n" BOARD_TEMPLATE = "\n " + "".join(COLUMN_LABELS) + "\n" + BOARD_EDGE + "\n" + (BOARD_ROW * BOARD_HEIGHT) + BOARD_EDGE لكن هذه الشيفرة غير قابلة للقراءة مثل سلسلة بسيطة متعددة الأسطر، ومن غير المرجح أن نغير حجم لوحة اللعبة على أي حال، لذلك سنستخدم السلسلة البسيطة متعددة الأسطر. نبدأ بكتابة الدالة main() التي ستستدعي جميع الدوال الأخرى التي أنشأناها لهذه اللعبة: def main(): """Runs a single game of Four-in-a-Row.""" print( """Four-in-a-Row, by Al Sweigart al@inventwithpython.com Two players take turns dropping tiles into one of seven columns, trying to make four-in-a-row horizontally, vertically, or diagonally. """ ) # Set up a new game: gameBoard = getNewBoard() playerTurn = PLAYER_X نعطي الدالة main() سلسلة توثيق نصية، قابلة للعرض viewable باستخدام دالة help() المضمنة. تُعِد الدالة main() أيضًا لوحة اللعبة للعبة جديدة وتختار اللاعب الأول. تحتوي الدالة main() حلقة لا نهائية: while True: # Run a player's turn. # Display the board and get player's move: displayBoard(gameBoard) playerMove = getPlayerMove(playerTurn, gameBoard) gameBoard[playerMove] = playerTurn يمثل كل تكرار لهذه الحلقة دورًا واحدًا. أولًا، نعرض لوحة اللعبة للاعب. ثانيًا، يختار اللاعب عمودًا لإسقاط حجر فيه، وثالثًا، نُحدث بنية بيانات لوحة اللعبة. بعد ذلك، نقيم نتائج حركة اللاعب: # Check for a win or tie: if isWinner(playerTurn, gameBoard): displayBoard(gameBoard) # Display the board one last time. print("Player {} has won!".format(playerTurn)) sys.exit() elif isFull(gameBoard): displayBoard(gameBoard) # Display the board one last time. print("There is a tie!") sys.exit() إذا أدّت حركة اللاعب لفوزه، ستعيد الدالة isWinner() القيمة True وتنتهي اللعبة؛ بينما إذا ملأ اللاعب اللوحة ولم يكن هناك فائز، ستعيد الدالة isFull() القيمة True وتنتهي اللعبة. لاحظ أنه بدلًا من استدعاء sys.exit()، كان بإمكاننا استخدام تعليمة break بسيطة. كان من الممكن أن يتسبب هذا في انقطاع التنفيذ عن حلقة while، ولأنه لا يوجد شيفرة برمجية في الدالة main() بعد هذه الحلقة، ستعود الدالة إلى استدعاء main() في الجزء السفلي من البرنامج، مما يتسبب في إنهاء البرنامج، لكننا اخترنا استخدام sys.exit() للتوضيح للمبرمجين الذين يقرؤون الشيفرة أن البرنامج سينتهي فورًا. إذا لم تنته اللعبة، تُعِد الأسطر التالية playerTurn للاعب الآخر: # Switch turns to other player: if playerTurn == PLAYER_X: playerTurn = PLAYER_O elif playerTurn == PLAYER_O: playerTurn = PLAYER_X لاحظ أنه كان بإمكاننا تحويل تعليمة elif إلى تعليمة else بسيطة دون شرط، لكن تذكر أن ممارسات بايثون الفُضلى تنص على "الصراحة أفضل من الضمنية explicit is better than implicit". تنص هذه الشيفرة صراحةً على أنه إذا جاء دور اللاعب O الآن، فسيكون دور اللاعب X هو التالي. ستنص الشيفرة البديلة على أنه إذا لم يكن دور اللاعب X الآن، فسيكون دور اللاعب X التالي. على الرغم من أن دوال if و else تتناسب بصورةٍ طبيعية مع الشروط المنطقية، لا تتطابق قيمتا PLAYER_X و PLAYER_O مع True وقيمة False: not PLAYER_X ليست PLAYER_O. لذلك، من المفيد أن تكون مباشرًا عند التحقق من قيمة playerTurn. بدلًا من ذلك، كان بإمكاننا تنفيذ جميع الإجراءات في سطر واحد: playerTurn = {PLAYER_X: PLAYER_O, PLAYER_O: PLAYER_X}[ playerTurn] يستخدم هذا السطر خدعة القاموس المذكورة في قسم "استخدام القواميس بدلا من العبارة Switch" في مقال الطرق البايثونية في استخدام قواميس بايثون ومتغيراتها وعاملها الثلاثي، ولكن مثل العديد من الأسطر الفردية، فهي غير سهلة القراءة مقارنةً بعبارة if و elif المباشرة. بعد ذلك، نعرّف الدالة getNewBoard(): def getNewBoard(): """Returns a dictionary that represents a Four-in-a-Row board. The keys are (columnIndex, rowIndex) tuples of two integers, and the values are one of the "X", "O" or "." (empty space) strings.""" board = {} for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): board[(columnIndex, rowIndex)] = EMPTY_SPACE return board تُعيد هذه الدالة قاموسًا يمثل لوحة أربع نقاط في صف واحد تربح؛ إذ يحتوي هذا القاموس على مجموعات (indexIndex و rowIndex) للمفاتيح (يمثّل العمود indexIndex و rowIndex أعدادًا صحيحة) و "X" أو "O" أو "." حرف الحجر في كل مكان على اللوحة. تُخزَّن هذه السلاسل في PLAYER_X و PLAYER_O و EMPTY_SPACE على التوالي. لعبة أربع نقاط في صف واحد تربح الخاصة بنا بسيطة نوعًا ما، لذا يُعد استخدام قاموس لتمثيل لوحة اللعبة أسلوبًا مناسبًا. ومع ذلك، كان بإمكاننا استخدام نهج كائني التوجه object-oriented بدلًا من ذلك. سنتعرف على البرمجة كائنية التوجه في الفصول القادمة. تأخذ دالة displayBoard() بنية بيانات لوحة اللعبة من أجل الوسيط board وتعرض اللوحة على الشاشة باستخدام ثابت BOARD_TEMPLATE: def displayBoard(board): """Display the board and its tiles on the screen.""" # Prepare a list to pass to the format() string method for the board # template. The list holds all of the board's tiles (and empty # spaces) going left to right, top to bottom: tileChars = [] تذكر أن BOARD_TEMPLATE هي سلسلة متعددة الأسطر بها عدة أزواج من الأقواس. عند استدعاء دالة format() على BOARD_TEMPLATE، ستُستبدل هذه الأقواس بقيم الوسطاء الممرّرة إلى format(). سيحتوي المتغير tileChars على قائمة بهذه الوسطاء. نبدأ بتخصيص قائمة فارغة لها، إذ ستحل القيمة الأولى في tileChars محل الزوج الأول من الأقواس في BOARD_TEMPLATE، وستحل القيمة الثانية محل الزوج الثاني، وهكذا. نشكّل قائمةً بالقيم من قاموس board: for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): tileChars.append(board[(columnIndex, rowIndex)]) # Display the board: print(BOARD_TEMPLATE.format(*tileChars)) تتكرر حلقات for المتداخلة هذه على كل صف وعمود محتملين على اللوحة، لتلحقهم بالقائمة في tileChars. بمجرد الانتهاء من هذه الحلقات، نمرر القيم الموجودة في قائمة tileChars بصورةٍ مفردة إلى التابع format() باستخدام محرف النجمة * في البادئة. يشرح قسم "استخدام * لإنشاء دوال مرنة" من مقال كتابة دوال فعالة في بايثون كيفية استخدام رمز النجمة للتعامل مع القيم الموجودة في قائمة مثل وسطاء دالة منفصلة، إذ تعادل الشيفرة print(*['cat', 'dog', 'rat']) الشيفرة print('cat', 'dog', 'rat'). نحتاج إلى النجمة لأن التابع format() يتوقع وسيطًا واحدًا لكل زوج من الأقواس، وليس وسيطًا واحدًا للقائمة الواحدة. بعد ذلك، نكتب دالة getPlayerMove(): def getPlayerMove(playerTile, board): """Let a player select a column on the board to drop a tile into. Returns a tuple of the (column, row) that the tile falls into.""" while True: # Keep asking player until they enter a valid move. print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:") response = input("> ").upper().strip() if response == "QUIT": print("Thanks for playing!") sys.exit() تبدأ الدالة بحلقة لا نهائية تنتظر أن يدخل اللاعب نقلة move صحيحة. تشبه هذه الشيفرة دالة getPlayerMove() في برنامج برج هانوي سابقًا. لاحظ أن استدعاء print() في بداية حلقة while loop يستخدم سلسلة نصية من النوع f، لذا لا يتعين علينا تغيير الرسالة إذا حدثنا BOARD_WIDTH. نتحقق من أن رد اللاعب هو عمود صالح؛ إذا لم يكن كذلك، تنقل دالة continue التنفيذ مرةً أخرى إلى بداية الحلقة لتطلب من اللاعب نقلة صحيحة: if response not in COLUMN_LABELS: print(f"Enter a number from 1 to {BOARD_WIDTH}.") continue # Ask player again for their move. كان من الممكن كتابة شرط التحقق من صحة الإدخال هذا على شكل: not response.isdecimal() or spam < 1 or spam > BOARD_WIDTH ولكن من الأسهل استخدام response not in COLUMN_LABELS. بعد ذلك، نحتاج إلى معرفة الصف الذي ستصل إليه الحجرة التي سقطت في العمود المحدد للاعب: columnIndex = int(response) - 1 # -1 for 0-based column indexes. # If the column is full, ask for a move again: if board[(columnIndex, 0)] != EMPTY_SPACE: print("That column is full, select another one.") continue # Ask player again for their move. تعرض اللوحة تسميات الأعمدة من 1 إلى 7 على الشاشة، بينما تستخدم فهارس (indexIndex، rowIndex) على اللوحة الفهرسة المستندة إلى 0، لذا فهي تتراوح من 0 إلى 6. لحل هذا التناقض، نحوّل قيم السلسلة '1' إلى '7' إلى القيم الصحيحة من 0 إلى 6. تبدأ فهارس الصفوف من 0 في أعلى اللوحة وتزيد إلى 6 في أسفل اللوحة. نتحقق من الصف العلوي في العمود المحدد لمعرفة ما إذا كان مشغولًا؛ وفي حال كان مشغولًا، فهذا العمود ممتلئ تمامًا وستعيد عبارة المتابعة التنفيذ إلى بداية الحلقة لتطلب من اللاعب نقلةً أخرى؛ وإذا لم يكن العمود ممتلئًا، فسنحتاج إلى العثور على أدنى مساحة غير مشغولة لنزول الحجر: # Starting from the bottom, find the first empty space. for rowIndex in range(BOARD_HEIGHT - 1, -1, -1): if board[(columnIndex, rowIndex)] == EMPTY_SPACE: return (columnIndex, rowIndex) تبدأ حلقة for من فهرس الصف السفلي، BOARD_HEIGHT - 1 أو 6، وتتحرك لأعلى حتى تعثر على أول مساحة فارغة. تُعيد الدالة بعد ذلك فهارس أدنى مساحة فارغة. في أي وقت تكون اللوحة ممتلئة، تنتهي اللعبة بالتعادل: def isFull(board): """Returns True if the `board` has no empty spaces, otherwise returns False.""" for rowIndex in range(BOARD_HEIGHT): for columnIndex in range(BOARD_WIDTH): if board[(columnIndex, rowIndex)] == EMPTY_SPACE: return False # Found an empty space, so return False. return True # All spaces are full. تستخدم الدالة isFull() زوجًا من حلقات for المتداخلة للمرور على كل مكان على اللوحة، وإذا عثرت على مساحة فارغة واحدة، فإن اللوحة ليست ممتلئة، وبالتالي تُعيد الدالة False. إذا نجح التنفيذ في المرور عبر كلتا الحلقتين، فإن الدالة isFull() لم تعثر على مساحة فارغة، لذا فإنها تعيد True. تتحقق دالة isWinner() ما إذا كان اللاعب قد فاز باللعبة أم لا: def isWinner(playerTile, board): """Returns True if `playerTile` has four tiles in a row on `board`, otherwise returns False.""" # Go through the entire board, checking for four-in-a-row: for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT): # Check for four-in-a-row going across to the right: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex)] tile3 = board[(columnIndex + 2, rowIndex)] tile4 = board[(columnIndex + 3, rowIndex)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True تُعيد هذه الدالة True إذا ظهر playerTile أربع مرات على التوالي أفقيًا أو رأسيًا أو قطريًا. لمعرفة استيفاء الشرط، يتعين علينا التحقق من كل مجموعة من أربع مسافات متجاورة على اللوحة، وسنستخدم سلسلةً من حلقات for المتداخلة لذلك. تمثل المجموعة (columnIndex, rowIndex) نقطة البداية، إذ نتحقق من نقطة البداية والمسافات الثلاثة على يمينها لسلسلة playerTile. إذا كانت مساحة البداية هي (columnIndex, rowIndex)، ستكون المسافة الموجودة على يمينها (columnIndex + 1, rowIndex)، وهكذا. سنحفظ المربعات الموجودة في هذه المساحات الأربعة في المتغيرات tile1 و tile2 و tile3 و tile4. إذا كانت كل هذه المتغيرات لها نفس قيمة playerTile، فقد وجدنا أربع نقاط في صف واحد، وتعيد الدالة isWinner() القيمة True. ذكرنا سابقًا في قسم "المتغيرات ذات اللواحق الرقمية" من مقال اكتشاف دلالات الأخطاء في شيفرات لغة بايثون أن الأسماء المتغيرات ذات اللواحق الرقمية المتسلسلة (مثل tile1 إلى tile4 في هذه اللعبة) تشير غالبًا إلى شيفرة ذات رائحة code smell تشير إلى أنه يجب عليك استخدام قائمة واحدة بدلًا من ذلك، لكن في هذا السياق، لا بأس بأسماء المتغيرات هذه؛ إذ لا نحتاج إلى استبدالها بقائمة، لأن برنامج الأربع نقاط في صف واحد سيتطلب دائمًا أربعة متغيرات تحديدًا. تذكر أن رائحة الشيفرة البرمجية لا تشير بالضرورة إلى وجود مشكلة؛ وهذا يعني فقط أننا يجب أن نلقي نظرة ثانية ونتأكد أننا كتبنا الشيفرة الخاصة بنا بطريقة أكثر قابلية للقراءة. قد يؤدي استخدام القائمة إلى جعل الشيفرة أكثر تعقيدًا في هذه الحالة، ولن تضيف أي فائدة، لذلك سنلتزم باستخدام tile1 و tile2 و tile3 و tile4. نستخدم عملية مماثلة للتحقق من وجود أربع أحجار متتالية رأسيًا: for columnIndex in range(BOARD_WIDTH): for rowIndex in range(BOARD_HEIGHT - 3): # Check for four-in-a-row going down: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex, rowIndex + 1)] tile3 = board[(columnIndex, rowIndex + 2)] tile4 = board[(columnIndex, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True بعد ذلك، نتحقق من وجود أربعة أحجار متتالية قطريًا للأسفل وإلى اليمين؛ ثم نتحقق من وجود أربعة أحجار متتالية قطريًا للأسفل وإلى اليسار: for columnIndex in range(BOARD_WIDTH - 3): for rowIndex in range(BOARD_HEIGHT - 3): # Check for four-in-a-row going right-down diagonal: tile1 = board[(columnIndex, rowIndex)] tile2 = board[(columnIndex + 1, rowIndex + 1)] tile3 = board[(columnIndex + 2, rowIndex + 2)] tile4 = board[(columnIndex + 3, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True # Check for four-in-a-row going left-down diagonal: tile1 = board[(columnIndex + 3, rowIndex)] tile2 = board[(columnIndex + 2, rowIndex + 1)] tile3 = board[(columnIndex + 1, rowIndex + 2)] tile4 = board[(columnIndex, rowIndex + 3)] if tile1 == tile2 == tile3 == tile4 == playerTile: return True هذه الشيفرة مشابهة لعمليات التحقق الأفقية، لذلك لن نكرر الشرح هنا. إذا فشلت جميع عمليات التحقق الخاصة بالعثور على تتاليات رباعية، تعيد الدالة False للإشارة إلى أن playerTile ليس فائزًا في هذه الحالة: return False الدالة الوحيدة المتبقية هي استدعاء دالة main(): # If this program was run (instead of imported), run the game: if __name__ == '__main__': main() نستخدم لغة بايثون الشائعة التي ستستدعي main() في حال تشغيل fourinarow.py مباشرةً، ولكن ليس في حال استيراد fourinarow.py مثل وحدة module. الخلاصة تستخدم لعبة أربع نقاط في صف واحد تربح محارف آسكي ASCII لعرض تمثيل للوحة اللعبة. نعرض هذا باستخدام سلسلة متعددة الأسطر مخزنة في ثابت BOARD_TEMPLATE. تحتوي هذه السلسلة على 42 زوجًا من الأقواس {} لعرض كل مسافة على لوحة بقياس 7×6. نستخدم الأقواس بحيث يمكن لتابع السلسلة format() استبدالها بالحجر الموجود في تلك المساحة. بهذه الطريقة، يصبح الأمر أكثر وضوحًا كيف تنتج سلسلة BOARD_TEMPLATE لوحة اللعبة كما تظهر على الشاشة. ترجمة -وبتصرف- لقسم من الفصل Practice Projects من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: برمجة لغز أبراج هانوي Hanoi Towers باستخدام لغة بايثون كتابة شيفرات بايثون Python: مبادئ بايثون التوجيهية العشرون وسوء استخدام الصيغة الشائع. إنشاء تطبيق ويب باستخدام إطار عمل فلاسك Flask من لغة بايثون بناء لعبة نرد بسيطة بلغة بايثون
-
يستخدم لغز أبراج هانوي Hanoi towers مكدسًا stack من الأقراص ذات أحجام مختلفة، وتحتوي هذه الأقراص على ثقوب في مراكزها، لذا يمكنك وضعها على أحد الأعمدة الثلاثة (الشكل 1). لحل اللغز، يجب على اللاعب نقل مجموعة الأقراص إلى أحد القطبين الآخرين. هناك ثلاثة قيود، هي: يمكن للاعب تحريك قرص واحد فقط في كل مرة. يمكن للاعب تحريك الأقراص من وإلى قمة البرج فقط. لا يمكن للاعب أبدًا وضع قرص أكبر فوق قرص أصغر. [الشكل 1: لعبة أبراج هانوي] حل هذا اللغز هو مشكلة شائعة في علوم الحاسوب ويُستخدم لتدريس الخوارزميات التعاودية recursive algorithms. لن يحل برنامجنا هذا اللغز، بل سيقدِّم اللغز للاعب بشري لحلها. يمكنك الاطلاع على معلومات إضافية حول خوارزمية أبراج هانوي والمتوفرة على موسوعة حسوب. خرج برنامج أبراج هانوي يعرض برنامج أبراج هانوي الأبراج على شكل محارف آسكي ASCII باستخدام أحرف نصية لتمثيل الأقراص. قد يبدو هذا الأمر بدائيًا مقارنةً بالتطبيقات الحديثة، لكنه يبقي التطبيق بسيطًا، لأننا نحتاج فقط إلى استدعاءات print() و input() للتفاعل مع المستخدم. عند تشغيل البرنامج، سيبدو الخرج مشابهًا لما يلي: THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com Move the tower of disks, one disk at a time, to another tower. Larger disks cannot rest on top of a smaller disk. More info at https://en.wikipedia.org/wiki/Tower_of_Hanoi || || || @_1@ || || @@_2@@ || || @@@_3@@@ || || @@@@_4@@@@ || || @@@@@_5@@@@@ || || A B C Enter the letters of "from" and "to" towers, or QUIT. (e.g., AB to move a disk from tower A to tower B.) > AC || || || || || || @@_2@@ || || @@@_3@@@ || || @@@@_4@@@@ || || @@@@@_5@@@@@ || @_1@ A B C Enter the letters of "from" and "to" towers, or QUIT. (e.g., AB to move a disk from tower A to tower B.) --snip-- || || || || || @_1@ || || @@_2@@ || || @@@_3@@@ || || @@@@_4@@@@ || || @@@@@_5@@@@@ A B C You have solved the puzzle! Well done! إذا كان عدد الأقراص n، يستغرق الأمر ما لا يقل عن 2n - 1 حركة لحل أبراج هانوي. يتطلب هذا البرج المكون من خمسة أقراص 31 خطوة كما يلي: AC, AB, CB, AC, BA, BC, AC, AB, CB, CA, BA, CB, AC, AB, CB, AC, BA, BC, AC, BA, CB, CA, BA, BC, AC, AB, CB, AC, BA, BC, AC. إذا كنت تريد حل تحدٍ أكبر بنفسك، فيمكنك زيادة متغير TOTAL_DISKS في البرنامج من 5 إلى 6. الشيفرة المصدرية افتح ملفًا جديدًا في المحرر أو البيئة التطويرية IDE، وأدخل الشيفرة التالية، ثم احفظ الملف باسم towerofhanoi.py: """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com A stack-moving puzzle game.""" import copy import sys TOTAL_DISKS = 5 # إضافة المزيد من الأقراص يجعل اللعبة أصعب # البدء بجميع الأقراص على البرج A SOLVED_TOWER = list(range(TOTAL_DISKS, 0, -1)) def main(): """تشغيل لعبة أبراج هانوي واحدة""" print( """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com Move the tower of disks, one disk at a time, to another tower. Larger disks cannot rest on top of a smaller disk. More info at https://wiki.hsoub.com/Algorithms/Towers_of_Hanoi """ ) """ يحتوي قاموس الأبراج على المفاتيح A و B و C وقيم كل من المفتاح هي قائمة تمثّل الأقراص الموجودة على البرج. تحتوي القائمة على أعداد صحيحة تمثل الأقراص التي تكون من أحجام مختلفة، وبداية القائمة هي أسفل البرج. في حال لعبة تحتوي على 5 أقراص، تمثّل القائمة [5,4,3,2,1] البرج المكتمل بينما تمثل القائمة [] برجًا لا يحتوي على أقراص. القائمة [1,3] تحتوي على قرص كبير في الأعلى وقرص صغير بالأسفل، وبالتالي فهي حالة غير صالحة. القائمة [3,1] هي حالة صالحة بما أن القرص الصغير هو أعلى القرص الكبير """ towers = {"A": copy.copy(SOLVED_TOWER), "B": [], "C": []} while True: # حلقة لدور واحد لكل تكرار من هذه الحلقة # اعرض الأقراص والأبراج displayTowers(towers) # اطلب من المستخدم أن يُدخل حركة fromTower, toTower = getPlayerMove(towers) # حرّك القرص الكبير من البرج fromTower إلى البرج toTower disk = towers[fromTower].pop() towers[toTower].append(disk) # تحقّق إذا حلّ المستخدم اللعبة if SOLVED_TOWER in (towers["B"], towers["C"]): displayTowers(towers) # اعرض الأبراج مرةً أخيرة print("You have solved the puzzle! Well done!") sys.exit() def getPlayerMove(towers): """اطلب من المستخدم أن يُدخل حركة، وأعد القيمتين fromTower وtoTower""" while True: # استمرّ بطلب إدخال حركة من المستخدم إلى أن يُدخل حركة صالحة print('Enter the letters of "from" and "to" towers, or QUIT.') print("(e.g., AB to move a disk from tower A to tower B.)") print() response = input("> ").upper().strip() if response == "QUIT": print("Thanks for playing!") sys.exit() # تأكّد أن المستخدم أدخل أحرف لأبراج موجودة if response not in ("AB", "AC", "BA", "BC", "CA", "CB"): print("Enter one of AB, AC, BA, BC, CA, or CB.") continue # اطلب حركة المستخدم مجددًا # استخدم أسماء متغيرات معبّرة fromTower, toTower = response[0], response[1] if len(towers[fromTower]) == 0: # لا يجب أن يكون برج "from" فارغًا print("You selected a tower with no disks.") continue # اطلب حركة من المستخدم مجددًا elif len(towers[toTower]) == 0: # يمكن لأي قرص أن يُحرّك إلى برج فارغ return fromTower, toTower elif towers[toTower][-1] < towers[fromTower][-1]: print("Can't put larger disks on top of smaller ones.") continue # اطلب حركة المستخدم مجددًا else: # حالة حركة صالحة؛ أعِد الأبراج المختارة return fromTower, toTower def displayTowers(towers): """اطبع الأبراج الثلاث مع أقراصها""" # اطبع الأبراج الثلاثة for level in range(TOTAL_DISKS, -1, -1): for tower in (towers["A"], towers["B"], towers["C"]): if level >= len(tower): displayDisk(0) # اعرض العمود الفارغ دون أقراص else: displayDisk(tower[level]) # اعرض القرص print() # اعرض تسميات الأبراج emptySpace = " " * (TOTAL_DISKS) print("{0} A{0}{0} B{0}{0} C\n".format(emptySpace)) def displayDisk(width): """أظهِر القرص مع العرض المحدّد، العرض بقيمة 0 يعني عدم وجود قرص""" emptySpace = " " * (TOTAL_DISKS - width) if width == 0: # اطبع قسم العمود الذي لا يحتوي على قرص print(f"{emptySpace}||{emptySpace}", end="") else: # اطبع القرص disk = "@" * width numLabel = str(width).rjust(2, "_") print(f"{emptySpace}{disk}{numLabel}{disk}{emptySpace}", end="") # اذا نُفّذ البرنامج (عوضًا عن استيراده)، ابدأ اللعبة if __name__ == "__main__": main() نفّذ هذا البرنامج والعب عدّة جولات للحصول على فكرة عما يفعله هذا البرنامج قبل قراءة شرح الشيفرة المصدرية. للتحقق من عدم وجود أخطاء كتابية، انسخها والصقها إلى أداة لكشف الاختلاف عبر الإنترنت. كتابة الشيفرة دعنا نلقي نظرةً على الشيفرة المصدرية لنرى كيف تتبع أفضل الممارسات والأنماط التي وضحناها سابقًا. نبدأ بالجزء العلوي من البرنامج: """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com A stack-moving puzzle game.""" يبدأ البرنامج بتعليق متعدد الأسطر يعمل مثل سلسلة توثيق نصية docstring للوحدة module المسماة towerofhanoi. ستستخدم دالة help() المضمنة هذه المعلومات لوصف الوحدة: >>> import towerofhanoi >>> help(towerofhanoi) Help on module towerofhanoi: NAME towerofhanoi DESCRIPTION THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com A stack-moving puzzle game. FUNCTIONS displayDisk(width) Display a single disk of the given width. --snip-- يمكنك إضافة المزيد من الكلمات، وحتى فقرات المعلومات، إلى سلسلة التوثيق النصية الخاصة بالوحدة إذا كنت بحاجة إلى ذلك. كتبنا هنا جزءًا صغيرًا فقط لأن البرنامج بسيط جدًا. تأتي تعليمات import بعد سلسلة توثيق الوحدة: import copy import sys يعمل منسّق السطور Black على تنسيق هذه التعليمات مثل سطور منفصلة بدلًا من سطر واحد مثل import copy, sys، وهذا يجعل إضافة أو إزالة الوحدات النمطية المستوردة أسهل عند التعامل مع أنظمة التحكم في الإصدار، مثل غيت Git، التي تتعقب التغييرات التي يجريها المبرمجون. بعد ذلك، نعرّف الثوابت constants التي سيحتاجها هذا البرنامج: TOTAL_DISKS = 5 # More disks means a more difficult puzzle. # Start with all disks on tower A: SOLVED_TOWER = list(range(TOTAL_DISKS, 0, -1)) نعرّف هذه الثوابت بالقرب من أعلى الملف لتجميعها معًا وجعلها متغيرات عامة. كتبنا أسماء الثوابت بأحرف كبيرة وبتنسيق نمط الثعبان snake_case لتمييزهم على أنهم ثوابت. يشير الثابت TOTAL_DISKS إلى عدد الأقراص التي يحتوي عليها اللغز، أما المتغير SOLVED_TOWER فهو مثال عن قائمة تحتوي على برج محلول؛ إذ تحتوي على كل قرص بحيث يكون أكبر قرص في الأسفل وأصغر قرص في الأعلى. نولّد هذه القيمة من قيمة TOTAL_DISKS، أما بالنسبة للأقراص الخمسة فهي [1, 2, 3, 4, 5]. لاحظ عدم وجود تلميحات حول الكتابة في هذا الملف، والسبب هو أنه يمكننا استنتاج أنواع جميع المتغيرات والمعاملات والقيم المُعادة من الشيفرة. على سبيل المثال، عيَّنا قيمة العدد الصحيح 5 للثابت TOTAL_DISKS، وبناءً على ذلك سيستنتج المدقق الكتابي، مثل Mypy، أن TOTAL_DISKS يجب أن يحتوي على أعداد صحيحة فقط. نعرّف الدالة main() التي يستدعيها البرنامج بالقرب من أسفل الملف: def main(): """Runs a single game of The Tower of Hanoi.""" print( """THE TOWER OF HANOI, by Al Sweigart al@inventwithpython.com Move the tower of disks, one disk at a time, to another tower. Larger disks cannot rest on top of a smaller disk. More info at https://wiki.hsoub.com/Algorithms/Towers_of_Hanoi """ ) يمكن أن تحتوي الدوال على سلاسل توثيق نصية أيضًا. لاحظ سلاسل التوثيق للدالة main() أسفل تعليمة def. يمكنك عرض هذا السلاسل عن طريق تنفيذ importofhanoi و help (towerofhanoi.main) من الصدفة التفاعلية. بعد ذلك، نكتب تعليقًا يصف مفصّلًا هيكل البيانات الذي نستخدمه لتمثيل البرج، لأنه يشكّل جوهر عمل هذا البرنامج: """The towers dictionary has keys "A", "B", and "C" and values that are lists representing a tower of disks. The list contains integers representing disks of different sizes, and the start of the list is the bottom of the tower. For a game with 5 disks, the list [5, 4, 3, 2, 1] represents a completed tower. The blank list [] represents a tower of no disks. The list [1, 3] has a larger disk on top of a smaller disk and is an invalid configuration. The list [3, 1] is allowed since smaller disks can go on top of larger ones.""" towers = {"A": copy.copy(SOLVED_TOWER), "B": [], "C": []} نستخدم قائمة SOLVED_TOWER مثل مكدس stack وهو أحد أبسط هياكل البيانات في تطوير البرمجيات؛ فالمكدس هو قائمة مرتبة من القيم التي تتغير فقط من خلال إضافة (تسمى أيضًا الدفع Pushing) أو إزالة (تسمى أيضًا السحب Popping) القيم من أعلى المكدس. يمثل هيكل البيانات البرج في برنامجنا. يمكننا تحويل قائمة بايثون إلى مكدس إذا استخدمنا تابع append() للدفع وتابع pop() للسحب، وتجنبنا تغيير القائمة بأي طريقة أخرى. سنتعامل مع نهاية القائمة على أنها أعلى المكدس. يمثل كل عدد صحيح في قائمة الأبراج قرصًا واحدًا بحجم معين. على سبيل المثال، في لعبة تحتوي على خمسة أقراص، تمثل القائمة [1, 2, 3, 4, 5] مجموعةً كاملةً من الأقراص من الأكبر ( رقم 5) في الأسفل وصولًا إلى الأصغر (رقم 1) في الأعلى. لاحظ أن تعليقنا يقدم أيضًا أمثلةً على كومة برج صالحة وغير صالحة. نكتب داخل الدالة main() حلقةً لا نهائية تُنفّذ لكل دور من جولة لعبة الألغاز الخاصة بنا: while True: # Run a single turn on each iteration of this loop. # Display the towers and disks: displayTowers(towers) # Ask the user for a move: fromTower, toTower = getPlayerMove(towers) # Move the top disk from fromTower to toTower: disk = towers[fromTower].pop() towers[toTower].append(disk) يرى اللاعب في كل دور حالة الأبراج ومن ثمّ يحدد الحركة التالية، ثم يحدّث البرنامج بعد ذلك بنية بيانات الأبراج. أخفينا تفاصيل هذه المهام في دالتي displayTowers() و getPlayerMove(). يسمح اسما الدالتين السابقتين الوصفيّين للدالة main() بتقديم نظرة عامة على ما يفعله البرنامج. تتحقق الأسطر التالية مما إذا كان اللاعب قد حل اللغز من خلال مقارنة البرج الكامل في SOLVED_TOWER بالقيمتين towers["B"] و towers["C"]: # Check if the user has solved the puzzle: if SOLVED_TOWER in (towers["B"], towers["C"]): displayTowers(towers) # Display the towers one last time. print("You have solved the puzzle! Well done!") sys.exit() لا نقارن القيمة مع towers["A"]، لأن هذا العمود يبدأ ببرج مكتمل فعلًا؛ ويحتاج اللاعب إلى تشكيل البرج على العمودين B أو C لحل اللغز. لاحظ أننا نعيد استخدام SOLVED_TOWER لإنشاء أبراج البداية والتحقق فيما إذا كان اللاعب قد حل اللغز. نظرًا لأن SOLVED_TOWER ثابت، يمكننا الوثوق في أنه سيحظى دائمًا بالقيمة التي خصصناها له في بداية الشيفرة المصدرية. الشرط الذي نستخدمه يعادل الصياغة التالية ولكنه أقصر منها: SOLVED_TOWER == towers["B"] or SOLVED_TOWER == towers["C"] وهي بأسلوب بايثون الذي ناقشناه سابقًا. إذا كان هذا الشرط صحيحًا، فقد حل اللاعب اللغز، وننهي البرنامج، وإلا فإننا ننفّذ تكرارًا آخر من الحلقة. تطلب دالة getPlayerMove() من اللاعب نقل القرص والتحقق من صحة هذه الخطوة بحسب قواعد اللعبة: def getPlayerMove(towers): """Asks the player for a move. Returns (fromTower, toTower).""" while True: # Keep asking player until they enter a valid move. print('Enter the letters of "from" and "to" towers, or QUIT.') print("(e.g., AB to move a disk from tower A to tower B.)") print() response = input("> ").upper().strip() نبدأ حلقة لا نهائية تستمر في التكرار حتى تتسبب تعليمةreturn في أن يترك التنفيذ الحلقة والدالة، أو ينهي استدعاء sys.exit() البرنامج. يطلب الجزء الأول من الحلقة من اللاعب إدخال حركة جديدة من خلال تحديد البرج الذي سينتقل منه القرص "from" إلى البرج الذي سينتقل القرص إليه "to". لاحظ تعليمة ()input("> ").upper().strip التي تتلقى مدخلات لوحة المفاتيح من اللاعب، إذ تقبل (" <")input إدخال النص من اللاعب من خلال الرمز <، الذي يشير إلى أنه يجب على اللاعب إدخال شيء ما، وقد يعتقد اللاعب أن البرنامج قد توقف إذا لم يوجد هذا الرمز. نستخدم تابع upper() على السلسلة المُعادة من input() لكي تُعيد صيغة الأحرف الكبيرة للسلسلة، ويسمح هذا للاعب بإدخال تسميات الأبراج بأحرف كبيرة أو صغيرة، مثل 'a' أو 'A' للبرج A، ثم يُستدعى تابع strip() على السلسلة الكبيرة، وإعادة السلسلة دون أي مسافات فارغة على أي من الجانبين في حال أضاف المستخدم مسافة عن طريق الخطأ عند إدخال حركته. تجعل سهولة الاستخدام هذه برنامجنا أسهل قليلًا على اللاعبين لاستخدامه. استمرارًا للدالة getPlayerMove()، نتحقق من الدخل الذي يدخله المستخدم: if response == "QUIT": print("Thanks for playing!") sys.exit() # Make sure the user entered valid tower letters: if response not in ("AB", "AC", "BA", "BC", "CA", "CB"): print("Enter one of AB, AC, BA, BC, CA, or CB.") continue # Ask player again for their move. إذا أدخل المستخدم QUIT (في أي حالة، وحتى مع وجود مسافات في بداية السلسلة النصية أو نهايتها، بسبب استدعاءات upper() و strip())، ينتهي البرنامج. كان من الممكن أن نجعل getPlayerMove() تُعيد 'QUIT' للإشارة إلى أنه على االلاعب استدعاء sys.exit() بدلًا من أن تستدعي الدالة getPlayerMove() التابع sys.exit()، لكن هذا من شأنه أن يعقّد القيمة المُعادة للدالة getPlayerMove(): إذ سيعيد ذلك إما مجموعةً من سلسلتين (لتحرُّك اللاعب) أو سلسلة واحدة 'QUIT'. الدالة التي تُعيد قيمًا من نوع بيانات واحد أسهل في الفهم من الدالة التي يمكنها إعادة قيم من العديد من الأنواع الممكنة. ناقشنا ذلك سابقًا في القسم "ينبغي على القيم المعادة أن تتضمن دوما نمط البيانات نفسه" من المقال البرمجة الوظيفية Functional Programming وتطبيقها في بايثون. من الممكن فقط تكوين ست مجموعات من الأبراج بين الأبراج الثلاثة، وعلى الرغم من أننا وفرنا القيمة في الشيفرة لجميع القيم الست بصورةٍ ثابتة في الحالة التي تتحقق من الحركة، ستكون قراءة الشيفرة أسهل بكثير من قراءة شيء مثل: len(response) != 2 or response[0] not in 'ABC' or response[1] not in 'ABC'or response[0] == response[1] في ظل هذه الظروف، يعدّ التوفير الثابت للقيم في الشيفرة هو النهج الأكثر وضوحًا. يُعد توفير القيم في الشيفرة، مثل "AB" و"AC" وقيم أخرى على أنها قيم سحرية صالحة فقط طالما أن البرنامج يحتوي على ثلاثة أعمدة ممارسةً سيئةً عمومًا، ولكن على الرغم من أننا قد نرغب في تعديل عدد الأقراص عن طريق تغيير ثابت TOTAL_DISKS، فمن المستبعد جدًا أن نضيف المزيد من الأعمدة إلى اللعبة، فلا بأس بكتابة كل خطوة ممكنة على هذا النحو. نُنشئ متغيرين جديدين fromTower و toTower مثل أسماء وصفية للبيانات، فهي لا تخدم غرضًا وظيفيًا، لكنها تجعل قراءة الشيفرة أسهل من قراءة response[0] و response[1]: # Use more descriptive variable names: fromTower, toTower = response[0], response[1] بعد ذلك، نتحقق مما إذا كانت الأبراج المحددة تشكل حركةً صالحة أم لا: if len(towers[fromTower]) == 0: # The "from" tower cannot be an empty tower: print("You selected a tower with no disks.") continue # Ask player again for their move. elif len(towers[toTower]) == 0: # Any disk can be moved onto an empty "to" tower: return fromTower, toTower elif towers[toTower][-1] < towers[fromTower][-1]: print("Can't put larger disks on top of smaller ones.") continue # Ask player again for their move. إذا لم تكن الحركة صالحة، ستعيد عبارة continue التنفيذ إلى بداية الحلقة، التي تطلب من اللاعب إدخال حركته مرةً أخرى. لاحظ أننا نتحقق فيما إذا كان toTower فارغًا؛ فإذا كان الأمر كذلك، فإننا نعيد fromTower, toTower للتأكيد إلى أن عملية النقل كانت صالحة، لأنه يمكنك دائمًا وضع قرص على عمود فارغ. يضمن هذان الشرطان الأولان أنه بحلول الوقت الذي يجري فيه فحص الشرط الثالث، لن تكون towers[toTower] و towers[fromTower] فارغةً أو تتسبب بحدوث خطأ IndexError. لقد طلبنا هذه الشروط بطريقة تمنع IndexError أو أي فحص إضافي. من المهم أن يتعامل برنامجك مع أي إدخال غير صالح من المستخدم أو حالات الخطأ المحتملة؛ فقد لا يعرف المستخدمون ماذا يدخلون، أو قد يخطئون في الكتابة، وبالمثل، قد تختفي الملفات بصورةٍ غير متوقعة، أو قد تتعطل قواعد البيانات. يجب أن تكون برامجك مرنة في مواجهة الحالات الاستثنائية، وإلا ستتعطل بصورةٍ غير متوقعة أو تتسبب في حدوث أخطاء دقيقة في وقت لاحق. إذا لم يكن أي من الشروط السابقة صحيحًا، فإن الدالة getPlayerMove() تُعيد fromTower, toTower: else: # This is a valid move, so return the selected towers: return fromTower, toTower تُعيد عبارات return دائمًا قيمة واحدة في بلغة بايثون. على الرغم من أن عبارة return هذه تبدو وكأنها تُعيد قيمتين، إلا أن بايثون تُعيد فعليًا مجموعةً واحدةً من قيمتين، وهو ما يعادل return (fromTower, toTower). يتجاهل مبرمجو بايثون الأقواس غالبًا في هذا السياق، إذ لا تعرّف الأقواس صفوفًا tuples كما تفعل الفواصل. لاحظ أن البرنامج يستدعي دالة getPlayerMove() مرةً واحدةً فقط من الدالة main(). لا تنقذنا الدالة من تكرار الشيفرة، وهو الغرض الأكثر شيوعًا لاستخدامها. لا يوجد سبب يمنعنا من وضع جميع الشيفرات في getPlayerMove() في الدالة main()، ولكن يمكننا أيضًا استخدام الدوال مثل طريقة لتنظيم الشيفرة في وحدات منفصلة، وهذه هي الطريقة التي نستخدم بها getPlayerMove()، إذ يؤدي ذلك إلى منع دالة main() من أن تصبح طويلة جدًا وغير عملية. تعرض دالة displayTowers() الأقراص الموجودة على الأبراج A و B و C في الوسيط towers: def displayTowers(towers): """Display the three towers with their disks.""" # Display the three towers: for level in range(TOTAL_DISKS, -1, -1): for tower in (towers["A"], towers["B"], towers["C"]): if level >= len(tower): displayDisk(0) # Display the bare pole with no disk. else: displayDisk(tower[level]) # Display the disk. print() تعتمد الدالة السابقة على دالة displayDisk()، التي سنغطيها تاليًا، لعرض كل قرص في البرج. تتحقق حلقة for level من كل قرص محتمل للبرج، وتتحقق حلقة for tower من الأبراج A و B و C. تستدعي دالة displayTowers() دالة displayDisk() لعرض كل قرص باتساع width معين، أو العمود الذي لا يحتوي على قرص في حال تمرير 0: # Display the tower labels A, B, and C: emptySpace = ' ' * (TOTAL_DISKS) print('{0} A{0}{0} B{0}{0} C\n'.format(emptySpace)) تعرض الشيفرة السابقة الأبراج A و B و C على الشاشة. يحتاج اللاعب إلى هذه المعلومات للتمييز بين الأبراج ولتعزيز أن الأبراج تحمل علامات A و B و C بدلًا من 1 و2 و3 أو يسار ومتوسط ويمين. اخترنا عدم استخدام 1 و2 و3 لاسم البرج لمنع اللاعبين من الخلط بين هذه الأرقام والأرقام المستخدمة لأحجام الأقراص. ضبطنا متغير emptySpace على عدد المسافات التي يجب وضعها بين كل تسمية، والتي بدورها تعتمد على TOTAL_DISKS، لأنه كلما زاد عدد الأقراص في اللعبة، كلما اتسعت المسافة بين العمودين. يمكننا استخدام تابع format() بدلًا من سلسلة f النصية، فيما يلي: print(f'{emptySpace} A{emptySpace}{emptySpace} B{emptySpace}{emptySpace} C\n') يتيح لنا ذلك استخدام الوسيط emptySpace نفسه في أي مكان يظهر فيه {0} في السلسلة المعنية، مما ينتج عنه شيفرة أقصر وأكثر قابلية للقراءة من إصدار سلسلة f. تعرض دالة displayDisk() قرصًا واحدًا مع اتساعه، وفي حالة عدم وجود قرص، فإنه يعرض العمود فقط: def displayDisk(width): """Display a disk of the given width. A width of 0 means no disk.""" emptySpace = ' ' * (TOTAL_DISKS - width) if width == 0: # Display a pole segment without a disk: print(f'{emptySpace}||{emptySpace}', end='') else: # Display the disk: disk = '@' * width numLabel = str(width).rjust(2, '_') print(f"{emptySpace}{disk}{numLabel}{disk}{emptySpace}", end='') نمثل هنا قرصًا يستخدم مساحةً فارغةً أولية، وعددًا من محارف "@" يساوي اتساع القرص، ومحرفين للاتساع (بما في ذلك شرطة سفلية إذا كان الاتساع رقمًا واحدًا)، وسلسلةً أخرى من محارف "@"، ثم مسافة فارغة لاحقة. لعرض العمود الفارغ فقط، كل ما نحتاجه هو المسافة الفارغة الأولية، وحرفي أنبوب pipe |، ومسافة فارغة لاحقة. نتيجةً لذلك، سنحتاج إلى ستة استدعاءات لعرض () displayDisk مع ستة وسطاء مختلفة للاتساع width للبرج التالي: || @_1@ @@_2@@ @@@_3@@@ @@@@_4@@@@ @@@@@_5@@@@@ لاحظ كيف تتقاسم دالتي displayTowers() و displayDisk() مسؤولية عرض الأبراج. على الرغم من أن displayTowers() تقرر كيفية تفسير هياكل البيانات التي تمثل كل برج، إلا أنها تعتمد على displayDisk() لعرض كل قرص في البرج فعليًا. يؤدي تقسيم البرنامج إلى دوال أصغر مثل هذه إلى تسهيل اختبار كل جزء. إذا كان البرنامج يعرض الأقراص بشكلٍ غير صحيح، فمن المحتمل أن تكون المشكلة في displayDisk()؛ أما إذا ظهرت الأقراص بترتيب خاطئ، فمن المحتمل أن تكون المشكلة في displayTowers()، وفي كلتا الحالتين، سيكون قسم الشيفرة الذي يتعين عليك تصحيحه أصغر بكثير. لاستدعاء الدالة main()، نستخدم دالة بايثون الشائعة: # If this program was run (instead of imported), run the game: if __name__ == '__main__': main() تعيّن بايثون تلقائيًا المتغير __name__ إلى '__main__' إذا شغل اللاعب برنامج towerofhanoi.py مباشرةً، ولكن إذا استورد شخص ما البرنامج مثل وحدة باستخدام import towerofhanoi، سيُعيَّن __name__على 'towerofhanoi'. سيستدعي السطر if __name__ == '__main__' : الدالة main()، إذا شغّل شخص ما برنامجنا، وبدأ لعبة برج هانوي، ولكن إذا أردنا ببساطة استيراد البرنامج مثل وحدة حتى نتمكن -على سبيل المثال- من استدعاء الدوال الفردية فيه لاختبار الوحدة، فسيكون هذا الشرط False ولن تُستدعى main(). الخلاصة نمثّل الأبراج الثلاثة في أبراج هانوي، مثل قاموس بمفاتيح 'A' و 'B' و 'C' وقيمها هي قوائم من الأعداد الصحيحة. ينجح هذا الأمر في برنامجنا ولكن إذا كان برنامجنا أكبر أو أكثر تعقيدًا، فسيكون من الجيد تمثيل هذه البيانات باستخدام الأصناف classes. لم نستخدم الأصناف وتقنيات البرمجة كائنية التوجه لأننا لم نناقش هذه المواضيع بعد، لكن ضع في الحسبان أنه من الجيد تمامًا استخدام صنف لهيكل البيانات هذا. تظهر الأبراج على أنها محارف آسكي ASCII على الشاشة، باستخدام أحرف نصية لإظهار كل قرص من الأبراج. ترجمة -وبتصرف- لقسم من الفصل Practice Projects من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: قياس درجة تعقيد شيفرة بايثون باستخدام ترميز Big O الخوارزميات التعاودية recursive algorithms التعاود recursion في جافا: حل مشكلة أبراج هانوي تعلم كتابة أكواد بايثون من خلال الأمثلة العملية
-
لتحديد ما هو تعقيد big O لجزء من الشيفرة الخاصة علينا إنجاز أربع مهام، هي: تحديد ما هي n، عدّ الخطوات في الشيفرة، إسقاط المراتب الدُنيا، وإسقاط المعاملات. مثلًا، لنجد big O الخاص بالدالة readingList(): def readingList(books): print('Here are the books I will read:') numberOfBooks = 0 for book in books: print(book) numberOfBooks += 1 print(numberOfBooks, 'books total.') تذكر أن n تُمثل حجم بيانات الدخل التي تعمل عليها الشيفرة. تعتمد n في الدوال عادةً على المعامل، ومعامل الدالة readingList() الوحيد هو books، لذا سيعطينا حجم books تصوّرًا جيدًا عن قيمة n، لأنه كلما زادت books يزداد وقت تنفيذ الدالة. الآن، لنعدّ الخطوات في هذه الشيفرة، ولكن ما يمكننا أن نعدّه خطوة STEP مبهمًا إلى حد ما، لكننا سنتبّع سطر الشيفرة بمثابة قاعدة. لدى الحلقات عدد خطوات بعدد التكرارات مضروبًا بعدد أسطر الشيفرة في الحلقة، ولمعرفة ماذا يعني ذلك، هذا تعداد خطوات الشيفرة داخل الدالة readingList(): def readingList(books): print('Here are the books I will read:') # خطوة واحدة numberOfBooks = 0 # خطوة واحدة for book in books: # n مضروبًا بعدد الخطوات في هذه الحلقة print(book) # خطوة واحدة numberOfBooks += 1 # خطوة واحدة print(numberOfBooks, 'books total.') # خطوة واحدة نعامل كل سطر من الشيفرة على أنه خطوة ما عدا حلقة for، إذ يُنفذ السطر مرةً لكل عنصر في books، ولأن n هي حجم books، يمكننا القول إنه ينفِّذ n خطوة. ليس هذا فقط بل إنه ينفِّذ كل الخطوات داخل الحلقة n مرة، لأن هناك خطوتان داخل الحلقة، يكون المجموع 2×n خطوة. يمكننا وصف خطواتنا على النحو التالي: def readingList(books): print('Here are the books I will read:') # خطوة واحدة numberOfBooks = 0 # خطوة واحدة for book in books: # n مضروبًا 2 خطوة print(book) # خطوة معدودة مسبقًا numberOfBooks += 1 # خطوة معدودة مسبقًا print(numberOfBooks, 'books total.') #خطوة واحدة عندما نحسب عدد الخطوات الكلي، سنحصل على 1+1+1+(n×2). يمكننا كتابة هذا التعبير على نحوٍ أبسط 2n+3. ليس الهدف من big O حساب دقائق الأمور بل هو مؤشر عام، لذا نُسقط المراتب الأدنى من العد. المرتبة 2n+3 هي (2n) خطية و 3 هو ثابت. إذا ابقينا فقط أعلى مرتبة نحصل على 2n، ثم نُسقط المعاملات من المرتبة؛ ففي 2n المعامل هو 2، وبعد إسقاطه سيتبقى لنا n، وهذا يعطينا big O النهائي للدالة readingList() الذي هو O(n) أو تعقيد وقت خطي. هذا الترتيب منطقي إذا فكرت فيه، فهناك عدة خطوات في الدالة الخاصة بنا، ولكن عمومًا إذا زادت قائمة books عشر مرات، يزداد وقت التنفيذ عشرة أضعاف أيضًأ. تغير زيادة books من 10 كتب إلى 100 كتاب الخوارزمية من 1 + (2 × 10) +1 + 1 أو 23 خطوة إلى 1 + (2 × 100) +1+ 1 أو 203 خطوات. الرقم 203 هو تقريبًا عشرة أضعاف 23، لذا يزداد وقت التنفيذ بالتناسب مع ازدياد n. لماذا تكون المراتب الدنيا والمعاملات غير مهمة؟ نُسقط المراتب الأدنى من عد الخطوات لأنها تُصبح أقل أهمية مع ازدياد حجم n؛ فإذا زدنا قائمة books في الدالة السابقة readingList() من 10 إلى 10,000,000,000 (10 مليار)، يزداد عدد الخطوات من 23 إلى 20,000,000,003 ولا تهم هذه الخطوات الثلاث الإضافية مع رقم كبير كهذا. لا يشكّل معاملًا coefficient كبيرًا لمرتبة صغيرة مع ازدياد كمية البيانات أي فرق مقارنة مع المراتب الأعلى، فعند حجم n معين، ستكون المراتب الأعلى دائما أبطأ من المراتب الأدنى، مثلًا لنقل إنه لدينا quadraticExample() الذي هو O(n2) وفيه 3n2 خطوة. لدينا أيضًا linearExample() الذي هو O(n) وفيه 1000n خطوة. لا يهم إذا كان المعامل 1000 أكبر من المعامل 3، فكلما زادت n ستكون بالنهاية العملية O(n2) أبطأ من العملية الخطية O(n). لا تهم الشيفرة تحديدًا ولكننا يمكن أن نفكر بها على النحو التالي: def quadraticExample(someData): # n هو حجم someData for i in someData: # n خطوة for j in someData: # n خطوة print('Something') # خطوة واحدة print('Something') # خطوة واحدة print('Something') # خطوة واحدة def linearExample(someData): # n هو حجم someData for i in someData: # n خطوة for k in range(1000): # 1000 خطوة print('Something') # خطوة معدودة مسبقًا لدى الدالة linearExample() معامل كبير (1000) مقارنة بالمعامل (3) الخاص بدالة quadraticExample. إذا كان حجم الدخل n هو 10، تظهر الدالة O(n2) أسرع فقط في الخطوات ال 300 الخاصة به مقارنةً بخطوات الدالة O(n) البالغ عددها 10000 خطوة. يهتم ترميز big O بصورةٍ أساسية بأداء الخوارزمية كلما تدرجت كمية العمل، فعندما يصل n إلى حجم 334 أو أكبر، ستكون الدالة quadraticExample() دائمًا أبطأ من الدالة linearExample()، حتى لو كانت الدالة ()linearExample تتطلب 1,000,000n خطوة، ستصبح الدالة quadraticExample() أبطأ عندما تصل n إلى 333,334. ستصبح العملية O(n2) أبطأ من O(n) أو المرتبة الأدنى. لمشاهدة ذلك لاحظ مخطط big O المبين بالشكل 3 التالي، الذي يبين كل مراتب ترميز big O. المحور X هو n حجم البيانات، والمحور y هو وقت التنفيذ اللازم لإنهاء العملية. [الشكل 3: مخطط مراتب big O] كما ترى، يزداد وقت التنفيذ بمعدل أسرع للمراتب الأعلى من المراتب الأدنى. على الرغم من أن المراتب الأدنى يمكن أن يكون لها معاملات أكبر مما يجعلها أكبر مؤقتًا من المراتب الأعلى، ستسبقهم المراتب الأعلى في النهاية. أمثلة عن تحليل Big O لنحدد مرتبة big O لبعض أمثلة الدوال، إذ سنستخدم في هذه الأمثلة المعامل المسمى books الذي هو قائمة سلسلة نصية لعناوين كتب. تحسب دالة countBookPoints() النتيجة score اعتمادًا على عدد books في قائمة الكتب، معظم الكتب قيمتها نقطة وبعض الكتب لكُتّاب معينين قيمتها نقطتين. def countBookPoints(books): points = 0 # خطوة واحدة for book in books: # n خطوة مضروبًا بعدد تكرارات الحلقة points += 1 # خطوة واحدة for book in books: # n خطوة مضروبًا بعدد تكرارات الحلقة if 'by Al Sweigart' in book: # خطوة واحدة points += 1 # خطوة واحدة return points # خطوة واحدة يصبح عدد الخطوات 1+ (n × 1) + (n × 2) + 1 الذي يصبح 3n + 2 بعد جمع الحدود المتشابهة، وبعد إسقاط المراتب الأدنى والمعاملات يصبح O(n) (تعقيد خطي)، حتى لو مررنا على books مرةً أو مرتين أو مليار مرة. تستخدم كل الأمثلة حتى الآن حلقةً واحدةً بتعقيد خطي، ولكن هذه الحلقات تتكرر n مرة. سنرى في الأمثلة اللاحقة أن حلقة وحيدة في الشيفرة لا تُمثل تعقيدًا خطيًا بينما تمثّل حلقةً تمرّ على كل البيانات الخاصة بك ذلك. تطبع دالة iLoveBooks() جملة "I LOVE BOOKS!!!" أو "BOOKS ARE GREAT!!!" عشر مرات: def iLoveBooks(books): for i in range(10): # 10 خطوات مضروبًا بعدد تكرارات الحلقة print('I LOVE BOOKS!!!') # خطوة واحدة print('BOOKS ARE GREAT!!!') # خطوة واحدة لدى هذه الدالة حلقة for ولكنها لا تمرّ على قائمة books وتجري عشرين خطوة مهما كان حجم books. يمكننا إعادة كتابة ذلك كما يلي: (1)20، وبعد إسقاط المعامل 20، يبقى لدينا O(1) أو تعقيد زمن ثابت. هذا منطقي لأن الدالة تستغرق نفس زمن التنفيذ مهما كان n حجم قائمة books. لاحقًا، لدينا الدالة cheerForFavoriteBook() التي تبحث في قائمة books لإيجاد كتاب مفضل: def cheerForFavoriteBook(books, favorite): for book in books: # n خطوة مضروبًا بعدد تكرار الحلقة print(book) # خطوة واحدة if book == favorite: # خطوة واحدة for i in range(100): # 100 خطوة مضروبًا بعدد خطوات الحلقة print('THIS IS A GREAT BOOK!!!') # خطوة واحدة تمرّ حلقة for book على قائمة books التي تتطلب n خطوة مضروبةً بعدد الخطوات داخل الحلقة، وتضم هذه الحلقة حلقة for i مضمّنة تتكرر مئة مرة، وهذا يعني أن حلقة for book تتكرر 102×n مرة أو 102n خطوة. سنجد بعد إسقاط المعامل أن cheerForFavoriteBook() هي عملية O(n) خطية. قد يكون المعامل 102 كبيرًا لكي يُهمل لكن خذ بالحسبان إذا لم تظهر favorite أبدًا في قائمة books ستُنفذ الدالة 1n خطوة فقط. يختلف تأثير المعاملات كثيرًا ولذا هي ليست ذات معنى كبير. تبحث الدالة findDuplicateBooks() في قائمة books (عملية خطية) مرةً لكل كتاب (عملية خطية أُخرى): def findDuplicateBooks(books): for i in range(books): # n خطوة for j in range(i + 1, books): # n خطوة if books[i] == books[j]: # خطوة واحدة print('Duplicate:', books[i]) # خطوة واحدة تمرّ حلقة for i على كل قائمة books وتُنفذ الخطوات داخل الحلقة n مرة. وتمرّ الحلقة for j على جزء من قائمة الكتب، على الرغم من أننا نُسقط المعاملات، لا تزال هذه العملية بتعقيد وقت خطي. هذا يعني أن حلقة for i تنجز n×n عملية أي n2، وهذا يعني أن الدالة findDuplicateBooks() هي عملية بوقت تنفيذ متعدد الحدود polynomial time operation أي O(n2). لا تشير الحلقات المتداخلة Nested loops لوحدها أنها عمليات متعددة الحدود، ولكن الحلقات المتداخلة التي تكرر فيها الحلقات n مرة تُنتج n2 خطوة، مما يدل على عملية O(n2). لنتابع على مثال أصعب؛ إذ تعمل عملية البحث الثنائي المذكورة سابقًا عن طريق البحث في منتصف القائمة المرتبة (سنسميها haystack) عن عنصر (سنسميه needle). إذا لم نجد needle هنا، سنكمل البحث في النصف التالي أو اللاحق من haystack اعتمادًا على أي نصف نتوقع فيه إيجاد needle فيه. نكرر هذه العملية عن طريق البحث عن الأنصاف الأصغر والأصغر حتى نجد needle أو ننهي العمل إذا لم تكن في haystack. لاحظ أن البحث الثنائي يعمل فقط مع العناصر في haystack مرتبة: def binarySearch(needle, haystack): if not len(haystack): # خطوة واحدة return None # خطوة واحدة startIndex = 0 # خطوة واحدة endIndex = len(haystack) - 1 # خطوة واحدة haystack.sort() # عدد غير معلوم من الخطوات while start <= end: # عدد غير معلوم من الخطوات midIndex = (startIndex + endIndex) // 2 # خطوة واحدة if haystack[midIndex] == needle: # خطوة واحدة # Found the needle. return midIndex # خطوة واحدة elif needle < haystack[midIndex]: # خطوة واحدة # Search the previous half. endIndex = midIndex - 1 # خطوة واحدة elif needle > haystack[mid]: # خطوة واحدة # Search the latter half. startIndex = midIndex + 1 # خطوة واحدة هناك سطرين في binarySearch() ليس من السهل عدهما، إذ يعتمد ترميز big O الخاص باستدعاء الدالة haystack.sort() على الشيفرة داخل وحدة sort() الخاصة ببايثون. ليست هذه الشيفرة سهلة الإيجاد ولكن يمكنك معرفة ترميز big O على الإنترنت والذي هو O(n log n). كل خوارزميات الترتيب العامة هي في أفضل الأحوال O(n log n). سنغطي ترميز big O لعدد من التوابع والدوال الخاصة بلغة بايثون في فقرة "مراتب Big O لاستدعاءات الدوال العامة" لاحقًا في هذا الفصل. حلقة while ليست بسيطة التحليل مثل حلقات for التي رأيناها، إذ علينا فهم خوارزمية البحث الثنائي لتحديد كم تكرار موجود في الحلقة. تغطي startIndex و endIndex قبل الحلقة كل مجال haystack وضُبطت midIndex في منتصف المجال. في كل تكرار لحلقة while يحصل واحد من شيئين: إذا كان haystack[midIndex] == needle نعرف أننا وجدنا needle وتُعيد الدالة الفهرس needle في haystack. إذا كان needle < haystack[midIndex] أو needle > haystack[midIndex] سيُنصّف المجال المُغطى من startIndex و endIndex، إما عن طريق تعديل startIndex أو endIndex. عدد المرات التي يمكن تقسيم أي قائمة حجمها n إلى النصف هو log2(n). لذا، لدى حلقة while مرتبة big O هي O(log n)K ولكن لأن مرتبة O(n log n) في سطر haystack.sort() هي أعلى من O(log n)، نسقط مرتبة O(log n) الأدنى وتصبح مرتبة big O لكل الدالة binarySearch() هي O(n log n). إذا ضمنا أن binarySearch() ستُستدعى فقط على قائمة مرتبة من haystack، يمكننا إزالة السطر haystack.sort() وجعل binarySearch() دالة O(log n). يحسّن هذا تقنيًا من كفاءة الدالة ولكن لا يجعل البرنامج أكثر كفاءة لأنه ينقل عمل الترتيب المطلوب إلى قسم آخر من البرنامج. تترك معظم تطبيقات البحث الثنائي خطوة الترتيب وبالتالي نقول أن خوارزميات البحث الثنائي لها تعقيد لوغاريتمي O(log n). مراتب Big O لاستدعاءات الدالة الشائعة يجب أن يأخذ تحليل big O مرتبة big O بالحسبان لأي دالة يجري استدعاؤها. إذا كتبت دالةً يمكنك تحليل الشيفرة الخاصة بك، ولكن لمعرفة مرتبة big O لدوال وتوابع بايثون المضمّنة يجب عليك إرجاعها إلى قوائم. تحتوي هذه القائمة مراتب big O لعمليات بايثون الشائعة لأنواع المتتاليات مثل السلاسل النصية والصفوف tuples والقوائم: s[i] reading and s[i] = value assignment هي عمليات O(1). s.append(value) هي عملية O(1). s.insert(i, value) هي عملية O(n). يحتاج إدخال قيم في متتالية (خاصة من المقدمة) إلى إزاحة كل القيم إلى الأعلى في الفهارس فوق i بمكان واحد في التسلسل. s.remove(value) هي عملية O(n). يحتاج إزالة قيم في متتالية (خاصة من المقدمة) إلى إزاحة كل القيم إلى الأسفل في الفهارس فوق I بمكان واحد في التسلسل. s.reverse() هي عملية O(n) لأنه يجب إعادة ترتيب كل عنصر في المتتالية. s.sort() هي عملية O(n log n) لأن خوارزمية الترتيب الخاصة ببايثون هي O(n log n). value in s هي عملية O(n) لأنه يجب التحقق من كل عنصر. :for value in s عملية O(n) len(s) هي عملية O(n) لأن بايثون يتابع كم عنصر موجود في المتتالية لذا لا يحتاج لإعادة عدهم عندما يُمرّر إلى len() تحتوي هذه القائمة على مراتب big O لعمليات بايثون الشائعة أنواع الربط مثل القواميس والمجموعات sets والمجموعات الجامدة frozensets: m[key] reading and m[key] = value assignment عمليات O(1). m.add(value) عملية O(1). value in m عمليات O(1) للقواميس التي هي أسرع باستخدام in مع المتتاليات. for key in m: عملية O(n). len(m) عملية O(1) لأن بايثون يتابع كم عنصر موجود في الربط لذا لا يحتاج لإعادة عدهم عندما يمرر إلى len(). على الرغم من أن القوائم تحتاج عمومًا إلى البحث عن كل العناصر من بدايتها حتى نهايتها، لكن القواميس تستخدم المفتاح لحساب العنوان والوقت اللازم للبحث عن قيمة المفتاح يبقى ثابتًا. يسمى هذا الاستدعاء خوارزمية التعمية Hashing Algorithm والعنوان يسمى التعمية hash. التعمية هي خارج نطاق هذا الكتاب ولكن يمكنك الاطلاع على مفهوم التعمية والدوال المرتبطة بها على موقع حسوب من خلال الرابط، إذ أنها السبب في جعل العديد من عمليات الربط بوقت ثابت O(1). تستخدم المجموعات التعمية أيضًا لأن المجموعات هي قواميس بحقيقتها ولكن بوجود مفاتيح بدلًا من أزواج مفتاح-قيمة. لكن تحويل القائمة إلى مجموعة هو عملية بتعقيد O(n) لذا لا يفيد تحويل القائمة إلى مجموعة ومن ثم الوصول إلى العناصر في تلك المجموعة. تحليل Big O بنظرة سريعة عندما تألف تنفيذ تحليل big O فأنت لست بحاجة لفعل كل الخطوات، إذ ستشاهد بعد فترة بعض الدلالات التي يمكن منها تحديد مرتبة big O بسرعة. لاحظ أن n هي حجم البيانات التي تعمل عليها الشيفرة، هذه بعض القواعد العامة المُستخدمة: إذا كانت الشيفرة لا تصل إلى أي بيانات هي O(1). إذا كانت الشيفرة تمر على البيانات تكون O(n). إذا كانت الشيفرة فيها حلقتين متداخلتين تمرّان على البيانات O(n2). لا تُعدّ استدعاءات الدالة خطوة واحدة بل هي عدد الخطوات داخل الدالة. راجع فقرة "مرتبة Big O لاستدعاءات الدالة العامة" في الأعلى. إذا كانت للشيفرة عملية "فرّق تسد" التي تنصف البيانات تكون O(log n). إذا كانت للشيفرة عملية "فرّق تسد" التي تُنفذ مرة لكل عنصر في البيانات تكون O(n log n). إذا كانت الشيفرة تمر على كل مجموعة ممكنة من القيم في البيانات n تكون O(n2) أو مرتبة أسية أُخرى. إذا كانت الشيفرة تمر على كل تبديل (أي ترتيب) للقيم في البيانات تكون O(n!). إذا كانت الشيفرة تتضمن ترتيب البيانات تكون على الأقل O(n log n). هذه القيم هي نقطة انطلاق جيدة ولكن لا يوجد بديل لتحليل big O. تذكر أن مرتبة big O ليست الحكم النهائي على الشيفرة إذا ما كانت سريعة أو بطيئة أو فعّالة. لنرى الدالة waitAnHour(): import time def waitAnHour(): time.sleep(3600) تقنيًا هذه الدالة waitAnHour() هي وقت ثابت O(1)، نفكّر دائمًا أن شيفرة الوقت الثابت سريعة، ولكن وقت تنفيذها هو ساعة. هل هذه الشيفرة فعّالة؟ لا، ولكن من الصعب تحسين برمجة دالة waitAnHour() تستطيع أن تُنفذ بأسرع من ساعة. ترميز Big O ليس بديلًا عن تحليل الشيفرة الخاصة بك، وإنما الهدف منه هو إعطاؤك نظرةً عن أداء الشيفرة مع زيادة كمية البيانات المُدخلة. لا يفيدنا Big O عندما تكون n صغيرة وعادة ما تكون n صغيرة ربما ستكون متسرعًا لتحليل كل قطعة شيفرة تكتبها مع هذه المعلومات عن ترميز big O. قبل أن تستخدم المطرقة التي بيدك (شيفرة بايثون) لكل مسمار تراه، خذ بالحسبان أن ترميز big O يفيد أكثر عندما يكون هناك كميات كبيرة من البيانات لمعالجتها، وفي الواقع أغلب كميات البيانات هي صغيرة، وفي مثل تلك الحالات، لا يستحق إنشاء خوارزميات مُنمقة ومتطورة مع مراتب big O منخفضة ذلك العناء. لدى مصمم لغة البرمجة جو Go روب بايك Rob Pike خمس قواعد عن البرمجة، واحدة منها هي: "الخوارزميات المنمّقة بطيئة عندما تكون 'n' صغيرة وبالعادة 'n' صغيرة". لن يواجه معظم مطورو البرمجيات مراكز بيانات كبيرة أو عمليات حسابية معقدة، بل برامج أبسط من ذلك، وفي هذه الحالات سيعطي تنفيذ الشيفرة مع محلًل معلومات profiler أدق عن أداء الشيفرة بدلًا من تحليل big O. الخلاصة خذ بالحسبان أن big O هي أداة تحليل مفيدة، ولكنها ليست بديل عن تنفيذ الشيفرة مع محلًل لمعرفة أين يوجد عنق الزجاجة، إذ تساعدك المعرفة بترميز big O وكيفية تباطؤ الشيفرة مع زيادة البيانات في تجنُّب كتابة الشيفرة في مراتب أبطأ من حاجتها. ترجمة -وبتصرف- لقسم من الفصل Measuring Performance And Big O Algorithm Analysis من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: ترميز Big O وحساب مراتب تعقيد الخوارزميات خوارزميات البحث المرجع الشامل إلى تعلم الخوارزميات للمبتدئين دليل شامل عن تحليل تعقيد الخوارزمية المرجع الشامل إلى تعلم لغة بايثون
-
بعد أن تعلمنا كيفية قياس سرعة البرامج في المقال السابق قياس أداء وسرعة تنفيذ شيفرة بايثون، سنتعلم كيفية قياس الزيادات النظرية theoretical increases في وقت التنفيذ runtime مع نمو حجم البيانات الخاصة بالبرنامج، ويُطلق على ذلك في علوم الحاسوب ترميز O الكبير big O notation. ربما يشعر مطورو البرامج الذين ليس لديهم خلفية في علوم الحاسوب التقليدية بوجود نقص في معارفهم، على الرغم من كون المعرفة في علوم الحاسوب مفيدة، لكنه ليس مرتبط مباشرةً مع تطوير البرمجيات. تُعدّ Big O نوعًا من خوارزميات التحليل التي تصف تعقيد الشيفرة مع زيادة عدد العناصر التي تعمل عليها. تصنف الشيفرة في مرتبة تصف عمومًا الوقت الذي تستغرقه الشيفرة لتُنفّذ، وكلما زادت تزيد كمية العمل الواجب إنجازه. يصف مطور لغة بايثون Python نيد باتشلدر Ned Batchelder خوارزمية big O بكونها تحليلًا لكيفية "تباطؤ الشيفرة كلما زادت البيانات" وهو عنوان حديثه في معرض PyCon 2018. لننظر إلى المثال التالي: لنفترض أنه لديك كميةٌ معينةٌ من العمل الذي يستغرق ساعةً ليكتمل، فإذا تضاعفت كمية العمل، كم سيستغرق من الوقت؟ ربما تعتقد أنه سيستغرق ضعف المدة ولكن الجواب الصحيح هو: يعتمد الأمر على نوع العمل المُنجز. إذا استغرقت ساعةً لقراءة كتاب قصير، فستستغرق أكثر أو أقل من ساعتين لقراءة كتابين قصيرين، وإذا كان بإمكانك ترتيب 500 كتاب أبجديًا، سيستغرق ترتيب 1000 كتاب أكثر من ساعتين لأنك ستبحث عن المكان المناسب لكل كتاب في مجموعة أكبر من الكتب. من ناحية أخرى، إذا أردت التأكد أن رف الكتب فارغ أو لا، لا يهم إذا كان هناك 0 أو 10 أو 1000 كتاب على الرف، فنظرةٌ واحدةٌ كافية لتعرف الجواب، إذ سيبقى وقت التنفيذ على حاله بغض النظر عن عدد الكتب الموجودة. يمكن أن يكون بعض الناس أسرع أو أبطأ في قراءة أو ترتيب الكتب أبجديًا ولكن تبقى هذه التوجهات العامة نفسها. تصف خوارزمية big O هذه التوجهات، إذ يمكن أن تُنفذ الخوارزمية على حاسوب سريع أو بطيء ولكننا نستطيع استخدام big O لوصف كيفية عمل الخوارزمية عمومًا، بغض النظر عن العتاد الذي ينفذ هذه الخوارزمية. لا تستخدم big O وحدات معينة مثل الثواني أو دورات المعالج لوصف وقت تنفيذ الخوارزمية لأنها تختلف بين أجهزة الحاسوب ولغات البرمجة. مراتب Big O يُعرّف ترميز Big O عادةً المراتب التالية، التي تتراوح من المنخفضة -التي تصف الشيفرة التي لا تتباطأ كثيرًا كلما زادت البيانات- إلى المراتب العليا -التي تصف الشيفرة الأكثر تباطؤًا: O(1) وقت ثابت (أدنى مرتبة) O(log n) وقت لوغاريتمي O(n) وقت خطي O(n log n) وقت N-Log-N O(n2) وقت متعدد الحدود O(2n) وقت أسي O(n!) وقت عاملي (أعلى مرتبة) لاحظ أن big O تستخدم حرف O كبير متبوعًا بقوسين يحتويان وصف المرتبة، إذ يمثّل حرف O الكبير المرتبة وتمثل n حجم دخل البيانات التي تعمل عليها الشيفرة. نلفظها "big oh of n" أو "big oh n". لا تحتاج لفهم المعنى الدقيق لكلمات مثل لوغاريتمي أو حدودي لاستخدام صيغة big O، إذ سنصِف كل نوع بالتفصيل في الفقرة التالية ولكن هذا تبسيط لهم: خوارزميات O(1) و O(log n) سريعة خوارزميات O(n) و O(n log n) ليست سيئة خوارزميات O(n2) و O(2n) بطيئة يمكن طبعًا مناقشة العكس في بعض الحالات ولكن هذه التوصيفات هي قواعد جيدة عمومًا، فهناك مراتب أكثر من big O المذكورة هنا ولكن هذه هي الأعم. دعنا نتحدث عن أنواع المهام التي يصفها كل نوع من هذه المهام. اصطلاح رف الكتب Bookshelf لمراتب Big O سنستمر في المثال التالي لمراتب big O باستخدام مصطلح رف الكتب، إذ تمثّل n عدد الكتب في الرف ويصف ترميز Big O كيف أن المهام المختلفة تستغرق وقتًا أطول كلما زاد عدد الكتب. تعقيد O(1): وقت ثابت معرفة "هل رف الكتب فارغ؟" هي عملية ذات وقت ثابت، إذ لا يهم عدد الكتب في الرف، فنظرةٌ واحدةٌ ستخبرنا ما إذا كان رف الكتب فارغ أم لا. يمكن أن يختلف عدد الكتب ولكن وقت التنفيذ يبقى ثابتًا، لأنه طالما رأينا كتابًا واحدًا على الرف يمكننا إيقاف البحث. القيمة n غير مهمة لسرعة تنفيذ المهمة لذا لا يوجد n في O(1). يمكنك أيضًا رؤية الوقت الثابت مكتوبًا (O(c أحيانًا . تعقيد O(log n): لوغاريتمي اللوغاريتم هو عكس الأس، الأس 24 أو 2×2×2×2 يساوي 16 ولكن اللوغاريتم log2(16) (تلفظ لوغاريتم أساس 2 للعدد 16) يساوي 4. نفترض في البرمجة قيمة الأساس 2 ولذلك نكتب O(log n) بدلًا من O(log2 n). البحث عن كتاب في رف كتب مرتب أبجديًا هي عملية لوغاريتمية الوقت؛ فمن أجل إيجاد كتاب واحد، يمكن التحقق من الكتاب في منتصف الرف، وإذا كان هو الكتاب المطلوب تكون قد انتهيت، وإذا لم يكن كذلك، يمكن تحديد إذا كان الكتاب قبل أو بعد الكتاب الذي في المنتصف. يقلل ذلك مجال البحث إلى النصف، ويمكنك تكرار هذه العملية مجددًا من خلال فحص الكتاب الذي في منتصف النصف الذي تتوقع فيه إيجاد الكتاب. نسمي ذلك خوارزمية بحث ثنائية binary search وهناك مثال على ذلك في "أمثلة عن ترميز Big O" لاحقًا. عدد المرات التي تستطيع قسم مجموعة n كتاب للنصف هي log2 n، في رف فيه 16 كتاب ستحتاج إلى 4 خطوات على الأكثر لإيجاد الكتاب الصحيح، لأن كل خطوة تقلل عدد الكتب التي يجب البحث فيها إلى النصف. يحتاج رف الكتب الذي فيه ضعف عدد الكتب فقط إلى خطوة إضافية واحدة للبحث عنه. إذا كان هناك 4.2 مليار كتاب في رف كتب مرتبة أبجديًا ستحتاج فقط إلى 32 خطوة لإيجاد كتاب معين. تتضمن خوارزميات Log n عادةً خطوة فرّق تسد divide and conquer وتنطوي على اختيار نصف دخل n للعمل عليه وبعدها نصف آخر من ذلك النصف وهكذا دواليك. تتدرج عمليات log n جيدًا، إذ يمكن أن يزداد العمل n إلى الضعف ولكن سيزداد وقت التنفيذ خطوةً واحدةً فقط. تعقيد O(n): وقت خطي تستغرق عملية قراءة كل الكتب على الرف وقتًا خطيًا؛ فإذا كانت الكتب بنفس الطول تقريبًا وضاعفت عدد الكتب على الرف، فسيستغرق ضعف الوقت تقريبًا لقراءة كل الكتب، ويزداد وقت التنفيذ بالتناسب مع عدد الكتب n. تعقيد O(n log n): وقت N-Log-N ترتيب الكتب أبجديًا هي عملية تستغرق وقت n-log-n. هذه المرتبة هي ناتج ضرب وقتي التنفيذ O(n) و O(log n) ببعضهما. يمكن القول أن مهمة O(n log n) هي مهمة O(log n) مع تنفيذها n مرة. فيما يلي تفسير بسيط حول ذلك. ابدأ بمجموعة كتب يجب ترتيبها أبجديًا ورف كتب فارغ، واتبع الخطوات في خوارزمية البحث الثنائي كما موضح في الفقرة السابقة "تعقيد O(log n): زمن لوغاريتمي" لمعرفة مكان كل كتاب على الرف. هذه عملية O(log n)، ومن أجل ترتيب n كتاب أبجديًا وكان كل كتاب بحاجة إلى log n خطوة لترتيبه أبجديًا، ستحتاج n×log n أو n log n خطوة لترتيب كل مجموعة الكتب أبجديًا. إذا تضاعف عدد الكتب سيستغرق أكثر من ضعف الوقت لترتيبهم أبجديًا، لذا تتدرج خوارزميات n log n جيدًا. خوارزميات الترتيب ذات الكفاءة، هي: O(n log n)، مثل ترتيب الدمج merge sort والترتيب السريع quicksort وترتيب الكومة heapsort وترتيب تيم Timsort (من اختراع تيم بيترز Tim Peters وهي الخوارزمية التي يستخدمها تابع sort() الخاص ببايثون). تعقيد O(n2): وقت متعدد الحدود التحقق من الكتب المكررة في رف كتب غير مرتب هي عملية تستغرق وقت حدودي polynomial time operation؛ فإذا كان هناك 100 كتاب يمكنك أن تبدأ بالكتاب الأول وتقارنه مع التسعة وتسعون الكتاب الباقين لمعرفة التشابه، ثم نأخذ ثاني كتاب ونتحقق بنفس الطريقة مثل باقي بقية الكتب التسعة وتسعون. خطوات التحقق من التكرار لكتاب واحد هي 99 (سنقرّب هذا الرقم إلى 100 الذي هو n في هذا المثال). يجب علينا فعل ذلك 100 مرة، مرةً لكل كتاب، لذا عدد خطوات التحقق لكل كتاب على الرف هو تقريبًا n×n أو n2. (يبقى هذا التقريب n2 صالحًا حتى لو كنا أذكياء ولم نكرر المقارنات). يزداد وقت التنفيذ بازدياد عدد الكتب تربيعيًا. سيأخذ التحقق من التكرار لمئة كتاب 100×100 أو 10,000 خطوة، ولكن التحقق من ضعف هذه القيمة أي 200 كتاب سيكون 200×200 أو 40,000 خطوة، أي أربع مرات عمل أكثر. وجد بعض الخبراء في كتابة الشيفرات الواقعية للعالم الحقيقي أن معظم استخدامات تحليل big O هي لتفادي كتابة خوارزمية O(n2) عن طريق الخطأ، عند وجود خوارزمية O(n log n) أو O(n). مرتبة O(n2) هي عندما تبدأ الخوارزميات بالتباطؤ كثيرًا، لذا معرفة أن الشيفرة الخاصة بك في مرتبة O(n2) أو أعلى يجب أن يجعلك تتوقف. ربما توجد خوارزمية مختلفة يمكنها حل المشكلة بصورةٍ أسرع، ويمكن في هذه الحالة أن يكون الإطلاع على قسم هيكلة البيانات والخوارزميات Data Structure and Algorithms -أو اختصارًا DSA- إما على أكاديمية حسوب مفيدًا. نسمي أيضًا O(n2) وقتًا تربيعيًا، ويمكن أن يكون لخوارزميات O(n3) وقتًا تكعيبيًا وهو أبطأ من O(n2) أو وقتًا رباعيًا O(n4) الذي هو أبطأ من O(n3) أو غيره من الأوقات الزمنية متعددة الحدود. تعقيد O(2n): وقت أسي أخذ صور لرف الكتب مع كل مجموعة ممكنة من الكتب هو عملية تستغرق وقتًا أُسّيًا. انظر لهذا الأمر بهذه الطريقة، كل كتاب على الرف يمكن أن يكون في الصورة أو لا يكون. يبين الشكل 1 كل مجموعة ممكنة، إذ تكون n هي 1 أو 2 أو 3. إذا كانت n هي 1 هناك طريقين للتجميع، إذا كانت n هي 2 هناك أربعة صور ممكنة، الكتابان على الرف، أو الكتابان ليسا على الرف، أو الكتاب الأول موجود والثاني ليس موجودًا، أو الكتاب الأول ليس موجودًا والثاني موجود. إذا أضفنا كتابًا ثالثًا، نكون قد ضاعفنا مرةً ثانية العمل المُراد فعله، لذا يجب عليك النظر إلى كل مجموعة فرعية لكتابين التي تضم الكتاب الثالث (أربعة صور) وكل مجموعة فرعية لكتابين دون الكتاب الثالث (أربعة صور أُخرى أي 23 أو 8 صور). يضاعف كل كتاب إضافي كمية العمل، فمن أجل n كتاب سيكون عدد الصور التي يجب أخذها (أي العمل الواجب فعله) هو 2n. [الشكل 1: مجموعة الكتب الممكنة على رف كتب من أجل كتاب واحد أو اثنين أو ثلاث كتب] يزداد وقت التنفيذ للمهام الأُسية بسرعة كبيرة. تحتاج ستة كتب إلى 26 أو 32 صورة، ولكن 32 كتاب يحتاج 232 أو أكثر من 4.2 مليار صورة. مرتبة O(22) أو O(23) أو O(24) وما بعدها هي مراتب مختلفة ولكنها كلها تحتوي تعقيدات وقت أُسّي. تعقيد O(n!): وقت عاملي أخذ صورةٍ لكل ترتيب معين هي عملية تستغرق وقتًا عاملي. نطلق على كل ترتيب ممكن اسم التبديل permutation من أجل n كتاب. النتيجة هي ترتيب n! أو n عاملي، فمثلًا 3! هي 3×2×1 أو 6. يبين الشكل 2 كل التبديلات الممكنة لثلاثة كتب. [الشكل 2: كل تبديلات !3 (أي 6) لثلاثة كتب على رف كتب] لحساب ذلك بنفسك، فكر بكل التبديلات الممكنة بالنسبة إلى n كتاب. لديك n خيار ممكن للكتاب الأول وبعدها n-1 خيار ممكن للكتاب الثاني (أي كل كتاب ما عدا المكان الذي اخترته للكتاب الأول) وبعدها n-2 خيار ممكن للكتاب الثالث وهكذا دواليك. مع 6 كتب تكون نتيجة !6 هي 6×5×4×3×2×1 أو 720 صورة. إضافة كتاب واحد آخر يجعل عدد الصور المطلوبة !7 أو 5.040. حتى من أجل قيم n صغيرة، تصبح خوارزميات الوقت العاملي مستحيلة الإنجاز في وقت منطقي، فإذا كان لديك 20 كتاب ويمكنك ترتيبهم وأخذ صورة كل ثانية، فستحتاج إلى وقت أكثر من عمر الكون للانتهاء من كل تبديل. واحدة من مشاكل O(n!) المعروفة هي معضلة مندوب المبيعات المسافر، إذ يجب على مندوب المبيعات أن يزور n مدينة ويريد حساب المسافة المقطوعة لكل مراتب n! الممكنة والتي يمكنه زيارتها، إذ يستطيع من تلك الحالات إيجاد أقصر طريق، ومن أجل منطقة بعدد كبير من المدن تصبح هذه المهمة مستحيلة الإنجاز في وقت منطقي. لحسن الحظ هناك خوارزميات مُحسنة لإيجاد طريق قصير (ولكن ليس من المضمون أن يكون الأقصر) بطريقة أسرع من O(n!). يحسب ترميز Big O الحالات الأسوأ يحسب Big O أسوأ حالة ممكنة لأي مهمة، إذ يحتاج إيجاد كتاب معين في رف كتب غير مُنظم مثلًا أن تبدأ من أحد الأطراف وتتحقق من الكتب حتى تجد الكتاب المطلوب. يمكن أن تكون محظوظًا ويكون الكتاب المطلوب هو أول كتاب تتحقق منه، ولكن ربما تكون سيء الحظ ويكون الكتاب الذي تريده هو آخر كتاب تتحقق منه، أو قد لا يكون موجودًا على الرف إطلاقًا. لذلك، في أفضل الحالات لا يهم إذا كان هناك مليارات الكتب التي يجب البحث فيها لأنك ستجد الكتاب الذي تريده مباشرةً، لكن هذا التفاؤل ليس مفيدًا في خوارزميات التحليل. تصف Big O ماذا يحصل في الحالات الأسوأ حظًا، أي إذا كان لديك n كتاب يجب عليك البحث في كل الكتب، ففي هذا المثال يزداد وقت التنفيذ بنفس معدل ازدياد عدد الكتب. يستخدم بعض المبرمجين ترميز big Omega لوصف الحالة الأفضل للخوارزمية، فمثلًا تعمل خوارزمية Ω(n) بكفاءة خطية في أفضل حالاتها وفي الحالة الأسوأ ربما تستغرق وقتًا أطول. تواجه بعض الخوارزميات حالات محظوظة جدًا، يحيث لا تعمل أي شيء، مثل إيجاد مسار الطريق لمكان أنت أصلًا فيه. يصف ترميز Big Theta الخوارزميات التي لها الترتيب نفسه في أسوأ وأفضل الحالات، فمثلًا تصف Θ(n) خوارزميةً لديها كفاءة خطية في أحسن وأسوأ الحالات، أي أنها خوارزمية O(n) وكذلك Ω(n). لا يُستخدم هذين الترميزين كثيرًا مثل استخدام big O ولكن تجدر معرفتهما. يُعد سماع الناس يتحدثون عن "big O الحالة الوسطية" عندما يعنون big Theta أو "big O الحالة الفُضلى" عندما يعنون big Omega أمرًا شائعًا رغم أنه متناقض؛ إذ تصف big O الحالة الأسوأ لوقت تنفيذ الخوارزمية تحديدًا، ولكن حتى لو كانت كلماتهم خاطئة لا تزال تفهم المعنى بغض النظر. العمليات الرياضية الكافية للتعامل مع Big O إذا كان علم الجبر لديك ضعيفًا فمعرفة العمليات الرياضية التالية أكثر من كافي عند التعامل مع big O: الضرب: تكرار الإضافة أي 2×4=8 هو مثل 2+2+2+2=8، وفي حال المتغيرات يكون n+n+n هو 3×n. ترميز الضرب: يهمل ترميز الجبر عادةً إشارة ×، لذا 2×n تُكتب 2n ومع الأرقام 3×2 تُكتب (3)2 أو ببساطة 6. خاصية الضرب بالعدد 1: ضرب أي عدد بالرقم 1 يُنتج الرقم نفسه أي 5=x15 و42 =x142 أو عمومًا n×1=n توزيع الضرب على الجمع: (3×2) + (3×2) = (4+3)2x كل طرف من المعادلة يساوي 14 أي عمومًا a(b+c) = ab+ac الأس: تكرار الضرب 16= 24 (تُلفظ "2 مرفوعة للقوة الرابعة تساوي 16") مثل 2×2×2×2= 16 هنا تكون 2 هي الأساس و 4 هي الأس. باستخدام المتغيرات n×n×n×n هي n4. يُستخدم في بايثون المعامل ** على سبيل المثال 2**4 تساوي 16. الأس الأول يساوي الأساس: 2= 21 و 9999=99991 وبصورةٍ عامة n1=n الأس 0 يساوي 1: 1= 20 و 1=99990 وبصورةٍ عامة n0=1 المعاملات: عوامل الضرب في 3n2+4n+5 المعاملات هي 3 و4 و5. يمكنك معرفة أن 5 هي معامل لأن 5 يمكن أن يُعاد كتابتها بالشكل (1)5 وأيضًا يمكن إعادة كتابتها 5n0. اللوغاريتمات: عكس الأس. لأن 16=24 نعرف أن log2(16)=4. نفول لوغاريتم الأساس 2 للعدد 16 هو 4. نستخدم في بايثون دالة math.log() إذ math.log(16, 2) تساوي إلى 4.0. يتطلب حساب big O تبسيط العمليات عن طريق جمع الحدود المتشابهة؛ والحد هو مجموعة من الأرقام والمتغيرات مضروبة مع بعضها، ففي 3n2+4n+5 تكون الحدود هي 3n2 و4n و5، إذ أن الحدود المتشابهة لديها نفس المتغير مرفوعًا لنفس القوة. في التعبير 3n2+4n+6n+5 الحدان 4n و6n هما متشابهان بإمكاننا التبسيط وإعادة الكتابة كالتالي 3n2+10n+5. خذ بالحسبان أنه يمكن كتابة 3n2+5n+4 على النحو التالي 3n2+5n+4(1)، إذ تطابق الحدود في هذا التعبير مرتبات big O التالية O(n2) و O(n) و O(1). سيفيد هذا لاحقًا عندما نُسقط المعاملات في حسابات big O. ستفيد هذه التذكرة عندما تحاول معرفة big O لقطعة من الشيفرة، ولكن لن تحتاجها بعد أن تنتهي من "تحليل Big O بنظرة سريعة" في المقال التالي. مفهوم big O بسيط ويمكن أن يفيد حتى لو لم تتبع القواعد الرياضية بصرامة. الخلاصة يُعد ترميز Big O من أكثر المفاهيم انتشارًا في علم الحواسيب للمبرمجين، وهو بحاجة لبعض المفاهيم في الرياضيات لفهمه، ولكن يمكن للمفهوم الأساسي، ألا وهو معرفة أن الشيفرة ستبطأ كلما زادت البيانات، أن يصف الخوارزميات دون الحاجة إلى أرقام كبيرة لحسابها. هناك سبع مراتب لترميز big O، وهي: O(1) أو الوقت الثابت، الذي يصف الشيفرة التي لا تتغير مع زيادة البيانات؛ و O(log n) أو الوقت اللوغاريتمي الذي يصف الشيفرة التي تزداد بخطوة كلما تضاعف عدد البيانات بمقدار n؛ و O(n) أو الوقت الخطي، الذي يصف الشيفرة التي تتباطأ يتناسب مع زيادة حجم البيانات بمقدار n؛ و O(n log n) أو وقت n-log-n، الذي يصف الشيفرة التي هي أبطأ من O(n) والعديد من خوارزميات الترتيب لديها هذه المرتبة. المراتب الأعلى هي أبطأ لأن وقت تنفيذها يزداد بصورةٍ أسرع من زيادة حجم دخل البيانات. يصف الوقت الحدودي O(n2) الشيفرة التي يزداد وقت تنفيذها بتربيع الدخل n. ليست المراتب O(2n) أو الوقت الأسّي، و O(n!) أو الوقت العاملي شائعة جدًا، ولكنها تأتي مع المجموعات والتبديلات على الترتيب. ترجمة -وبتصرف- لقسم من الفصل Measuring Performance And Big O Algorithm Analysis من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: قياس أداء وسرعة تنفيذ شيفرة بايثون -تحليل الخوارزميات تعقيد الخوارزميات Algorithms Complexity دليل شامل عن تحليل تعقيد الخوارزمية
-
لا يهم الأداء كثيرًا من أجل البرامج الصغيرة، فربما تستغرق ساعةً في كتابة برنامج نصي لأتمتة مهمة تحتاج ثواني لتُنفذ. حتى لو استغرقت وقتًا أطول فسينتهي البرنامج عندما تعود لمكتبك مع فنجان القهوة، إلا أنه من الضروري أحيانًا الاهتمام بتعلم كيفية جعل البرامج النصية أسرع، ولكن لا نستطيع معرفة إذا كان التغييرات قد حسّنت البرنامج إذا لم نكن نعرف كيفية قياس سرعة البرنامج. يأتي هنا دور وحدات مثل timeit و cProfile الخاصة بلغة بايثون، إذ تقيس هذه الوحدات سرعة تنفيذ الشيفرة فقط وتُنشئ أيضًا توصيفًا لأجزاء الشيفرة السريعة والأجزاء التي تحتاج إلى تحسين. سنتعلم في هذا الفصل -إضافةً إلى قياس سرعة البرنامج- إلى كيفية قياس الزيادات النظرية theoretical increases في وقت التنفيذ runtime مع نمو حجم البيانات الخاصة ببرنامجك. يُطلق على ذلك في علوم الحاسوب ترميز O الكبير big O notation. وحدة timeit تُعد مقولة "التحسين السابق لأوانه هو أصل كل شر Premature optimization is the root of all evil" مقولةً شائعةً في تطوير البرمجيات، والتي تُنسب إلى عالم الحاسوب دونالد نوث Donald Knuth، الذي ينسبها بدوره إلى طوني هوري Tony Hoare. وهو بدوره ينسبها إلى دونالد نوث Donald Knuth. تظهر أهمية التحسين السابق لأوانه Premature optimization أو التحسين قبل معرفة ما يجب تحسينه، عندما يستخدم المبرمجون خدعًا ذكيةً لتوفير الذاكرة وكتابة الشيفرة بصورةٍ أسرع. مثال عن هذه الخدع هي استخدام خوارزمية XOR للتبديل بين عددين صحيحين دون استخدام عدد ثالث مثل متغير مؤقت. >>> a, b = 42, 101 # ضبط المتغيرَين >>> print(a, b) 42 101 >>> # ستبدّل سلسلة من عمليات XOR قيمتَي المتغيرَين >>> a = a ^ b >>> b = a ^ b >>> a = a ^ b >>> print(a, b) # بُدّلت القيم الآن 101 42 تبدو هذه الشيفرة مبهمة إذا لم تكن خوارزمية XOR مألوفة لديك (التي تستخدم المعامل الثنائي ^). المشكلة في استخدام خدع برمجية ذكية أنها تُنتج شيفرةً معقدةً وغير مقروءة، وذكرنا سابقًا أنّ أحد نقاط بايثون المهمة هي قابلية القراءة. في حالات أسوأ، يمكن ألا تكون الخدع الذكية ذكيةً إطلاقًا، إذ لا يمكن افتراض أن هذه الخدع أسرع أو أن الشيفرة التي تستبدلها هي بالأساس بطيئة. الطريقة الوحيدة لمعرفة ذلك هي قياس ومقارنة وقت التنفيذ، الذي هو الوقت الذي يستغرقه البرنامج لتنفيذ البرنامج أو قطعة من الشيفرة البرمجية. يجب أخذ العلم أن زيادة وقت التنفيذ يعني أن البرنامج يتباطأ؛ أي يستغرق وقتًا أطول لتنفيذ نفس كمية العمل (نستخدم أيضًا مصطلح "وقت التنفيذ" ليعني الوقت الذي يكون به البرنامج عاملًا. عندما نقول أن الخطأ قد حصل وقت التنفيذ، يعني أن الخطأ حصل عندما كان البرنامج يعمل وليس عندما كان يُصرّف إلى شيفرة ثنائية bytecode. يمكن لوحدة timeit الخاصة بالمكتبة القياسية لبايثون قياس سرعة وقت التنفيذ لأجزاء صغيرة من الشيفرة عن طريق تنفيذ الشيفرة آلاف أو ملايين المرات والسماح لك بتحديد وقت التنفيذ الوسطي. تعطِّل أيضًا وحدة timeit كانس المهملات garbage collector التلقائي للحصول على أوقات تنفيذ ثابتة. يمكنك تمرير سلسلة نصية متعددة الأسطر أو فصل أسطر الشيفرة باستخدام الفاصلة المنقوطة إذا أردت اختبار عدة أسطر: >>> import timeit >>> timeit.timeit('a, b = 42, 101; a = a ^ b; b = a ^ b; a = a ^ b') 0.1307766629999998 >>> timeit.timeit("""a, b = 42, 101 ... a = a ^ b ... b = a ^ b ... a = a ^ b""") 0.13515726800000039 تستغرق خوارزمية XOR على جهاز الحاسوب الخاص بي حوالي عشر الثانية لتنفيذ الشيفرة، هل هذا سريع؟ لنقارنها مع شيفرة تبديل الأعداد الصحيحة التي تستخدم متغير ثالث. >>> import timeit >>> timeit.timeit('a, b = 42, 101; temp = a; a = b; b = temp') 0.027540389999998638 هذه مفاجأة، ليست خوارزمية المتغير الثالث أسهل للقراءة فقط، لكنها أسرع بمرتين. خدعة XOR الذكية ربما توفر بعض البايتات من الذاكرة ولكن على حساب السرعة وسهولة القراءة. التضحية بسهولة قراءة الشيفرة لتوفير بعض البايتات من استخدام الذاكرة أو بضعة أجزاء من الثانية من وقت التنفيذ ليس بهذا القدر من الأهمية. المفاجأة الأفضل هي عند التبديل بين متغيرين باستخدام خدعة الإسناد المتعدد multiple assignment أو التفريغ المكرّر iterable unpacking التي تُنفذ أيضًا في وقت قصير: >>> timeit.timeit('a, b = 42, 101; a, b = b, a') 0.024489236000007963 ليس هذه الشيفرة هي الأسهل للقراءة فقط، لكنها الأسرع. عرفنا ذلك ليس لأننا افترضنا ولكن لأننا قسنا ذلك بموضوعية. يمكن أن تأخذ دالة timeit.timeit() وسيط سلسلة نصية ثانٍ من شيفرة SETUP. تُنفَّذ شيفرة الإعداد هذه مرةً واحدةً قبل تنفيذ أول سلسلة نصية من الشيفرة. يمكن تغيير عدد المحاولات بتمرير عدد صحيح لوسيط الكلمة المفتاحية number. يختبر المثال التالي سرعة وحدة random الخاصة ببايثون لإنشاء عشرة ملايين رقم عشوائي من 1 إلى 100، وذد استغرق ذلك حوالي 10 ثوان على جهاز حاسوب ما. >>> timeit.timeit('random.randint(1, 100)', 'import random', number=10000000) 10.020913950999784 قياسيًا، لا تستطيع الشيفرة في السلسلة النصية المُمررة إلى timeit.timeit() الوصول إلى المتغيرات والدوال في باقي البرنامج: >>> import timeit >>> spam = 'hello' #نعرّف المتغير spam >>> timeit.timeit('print(spam)', number=1) # نقيس الوقت المستغرق لطباعة المتغير spam Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Users\Al\AppData\Local\Programs\Python\Python37\lib\timeit.py", line 232, in timeit return Timer(stmt, setup, timer, globals).timeit(number) File "C:\Users\Al\AppData\Local\Programs\Python\Python37\lib\timeit.py", line 176, in timeit timing = self.inner(it, self.timer) File "<timeit-src>", line 6, in inner NameError: name 'spam' is not defined لإصلاح ذلك، مرّر الدالة والقيمة المُعادة للدالة globals() إلى وسيط الكلمة المفتاحية globals: >>> timeit.timeit('print(spam)', number=1, globals=globals()) hello 0.000994909999462834 قاعدة جيدة في كتابة الشيفرة هي أن تجعل الشيفرة تعمل ومن ثم جعلها سريعة، إذ يمكنك التركيز على جعل الشيفرة أكثر كفاءة بعد الحصول على شيفرة تعمل. فحص الأداء بواسطة cProfile على الرغم من أن وحدة timeit مفيدة لقياس أجزاء صغيرة من الشيفرة، إلا أن وحدة cProfile مفيدةٌ أكثر في تحليل دوال أو برامج كاملة. يحلًل فحص الأداء Profiling سرعة واستخدام الذاكرة وبعض النواحي الأخرى للبرنامج الخاص بك. تُعد وحدة cProfile هي فاحص الأداء profiler الخاص ببايثون أو البرنامج الذي يستطيع قياس وقت تنفيذ البرنامج، إضافةً لإنشاء توصيف لأوقات تنفيذ استدعاءات دوال البرنامج كلٌّ على حِدى. تقدم هذه المعلومات قياسات أكثر دقة للشيفرة الخاصة بك. يمرر محلًل cProfile سلسلةً نصيةً من الشيفرة التي تريد قياسها إلى cProfile.run(). لنتابع كيف يقيس cProfiler ويعطي تقريرًا عن تنفيذ دالة قصيرة تجمع كل الأرقام من 1 إلى 1,000,000: import time, cProfile def addUpNumbers(): total = 0 for i in range(1, 1000001): total += i cProfile.run('addUpNumbers()') يكون الخرج عند تنفيذ البرنامج على النحو التالي: 4 function calls in 0.064 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.000 0.000 0.064 0.064 <string>:1(<module>) 1 0.064 0.064 0.064 0.064 test1.py:2(addUpNumbers) 1 0.000 0.000 0.064 0.064 {built-in method builtins.exec} 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} يمثل كل سطر دالةً مختلفةً والوقت المستغرق في تلك الدالة. تكون الأعمدة في خرج cProfile.run() على النحو التالي: ncalls: عدد استدعاءات الدالة. tottime: الوقت الكلي المستغرق في الدالة ما عدا الوقت في الدوال الفرعية. percall: الوقت الكلي مقسومًا على عدد الاستدعاءات. cumtime: الوقت التراكمي المستغرق في الدالة ولك الدوال الفرعية. percall: الوقت التراكمي مقسومًا على عدد الاستدعاءات. filename:lineno(function): الملف الذي فيه الدالة وفي أي رقم سطر. مثال: نزّل الملفين "rsaCipher.py" و "al_sweigart_pubkey.txt" من الموقع. أدخل ما يلي على الصدفة التفاعلية لتحليل دالة encryptAndWriteToFile() أثناء تشفير رسالة مكونة من 300,000 محرفًا ومُنشأة باستخدام التعبير 'abc' * 100000: >>> import cProfile, rsaCipher >>> cProfile.run("rsaCipher.encryptAndWriteToFile('encrypted_file.txt', 'al_sweigart_pubkey.txt', 'abc'*100000)") 11749 function calls in 28.900 seconds Ordered by: standard name ncalls tottime percall cumtime percall filename:lineno(function) 1 0.001 0.001 28.900 28.900 <string>:1(<module>) 2 0.000 0.000 0.000 0.000 _bootlocale.py:11(getpreferredencoding) --snip-- 1 0.017 0.017 28.900 28.900 rsaCipher.py:104(encryptAndWriteToFile) 1 0.248 0.248 0.249 0.249 rsaCipher.py:36(getBlocksFromText) 1 0.006 0.006 28.873 28.873 rsaCipher.py:70(encryptMessage) 1 0.000 0.000 0.000 0.000 rsaCipher.py:94(readKeyFile) --snip-- 2347 0.000 0.000 0.000 0.000 {built-in method builtins.len} 2344 0.000 0.000 0.000 0.000 {built-in method builtins.min} 2344 28.617 0.012 28.617 0.012 {built-in method builtins.pow} 2 0.001 0.000 0.001 0.000 {built-in method io.open} 4688 0.001 0.000 0.001 0.000 {method 'append' of 'list' objects} --snip-- يمكنك ملاحظة أن الشيفرة التي مررناها إلى cProfile.run() استغرقت 28.9 ثانية لتنتهي. انتبه إلى الدوال بأطول الأوقات الكلية، وفي حالتنا هي الدالة pow() التي تستغرق 28.617 ثانية، وهذا تقريبًا هو كل وقت تنفيذ الشيفرة. لا يمكن تعديل هذه الشيفرة (هي جزء من بايثون) ولكن ربما نستطيع الاعتماد بصورةٍ أقل عليها، وهذا غير ممكن في هذه الحالة، لأن برنامج rsaCipher.py مُحسن جيدًا، إلا أن تحليل هذه الشيفرة أعطانا نظرةً إلى أن عنق الزجاجة الأساسي هو pow() لذا لا يوجد فائدة من محاولة تحسين الدالة readKeyFile() التي لا تستغرق وقت تنفيذ أبدًا حتى أن cProfile أعطانا وقت لتنفيذها يبلغ 0. هذه الفكرة موجودة في قانون أمدال Amdahl's Law وهي معادلة تحسب كيف يُسرّع البرنامج إذا حسّننا أحد أجزائه، فوفقًا لمعادلة أمدال يكون تسريع المهمة الكلي مساويًا إلى: 1 / ((1 – p) + (p / s)) إذ يمثّل s التسريع الحاصل لأحد الأجزاء، و p هو نسبة ذلك الجزء من كل البرنامج، أي إذا ضاعفنا سرعة أحد الأجزاء الذي يشكل 90% من وقت تنفيذ البرنامج سنحصل على تسريع بنسبة 82% لكل البرنامج: 1 / ((1 – 0.9) + (0.9 / 2)) = 1.818 وهذا أفضل من تسريع جزء بمقدار ثلاث أضعاف، ولكنه يشكّل 25% من وقت التنفيذ الكلي، الذي يعطي نسبة 14% تسريع كلي: 1 / ((1 – 0.25) + (0.25 / 2)) = 1.143 لست بحاجة حفظ هذه المعادلة فقط تذكر أن مضاعفة سرعة أجزاء الشيفرة البطيئة أو الطويلة مفيدٌ أكثر من مضاعفة سرعة قسم قصير أو سريع. هذه يجب أن تكون معرفة عامة، حسم 10% من بيت باهظ الثمن أفضل من حسم 10% من زوج أحذية رخيص. الخلاصة تأتي مكتبة بايثون القياسية مع وحدتين للتحليل timeit و cProfiler. تفيد الدالة time.timeit() في تنفيذ قطع صغيرة من الشيفرة للمقارنة بين سرعة كل قطعة منها. تقدم دالة cProfile.run() تقريرًا مفصلًا للتوابع الأكبر وتدل على وجود عنق زجاجة. من المهم قياس أداء الشيفرة الخاصة بك بدلًا من تقدير ذلك، إذ يمكن لبعض حيل تسريع البرامج إبطائه بالحقيقة، أو ربما تستغرق وقتًا أطول في تحسين ما هو جزء بسيط من البرنامج الخاص بك. يوضح ذلك قانون أمدال رياضيًا، إذ تصف هذه المعادلة أثر تسريع مكون واحد على كل البرنامج. ترجمة -وبتصرف- لقسم من الفصل Measuring Performance And Big O Algorithm Analysis من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: استخدامات متقدمة لنظام التحكم بالإصدار Git لإدارة مشاريع بايثون الوحدات Modules والحزم Packages في بايثون أنواع البيانات والعمليات الأساسية في لغة بايثون
-
أنظمة التحكم في الإصدار version control هي أدوات تسجّل جميع تغيرات الشيفرة المصدرية وتجعل من السهل استرداد الإصدارات القديمة من الشيفرات البرمجية، ويمكنك النظر إلى هذه الأدوات بكونها أدوات متطورة للتراجع عن فعل ما؛ فعلى سبيل المثال، إذا استبدلت دالةً ثم قررت لاحقًا تفضيل الدالة القديمة، يمكنك استعادة شيفرتك إلى الإصدار الأصلي، أو في حال اكتشفت خطأً جديدًا، فيمكنك العودة إلى الإصدارات السابقة لتحديد وقت ظهوره لأول مرة وأي تغيير في الشيفرة تسبب بحدوثه. يدير نظام التحكم في الإصدار الملفات أثناء إجراء التغييرات عليها، ويشابه ذلك عمل نسخة من مجلد "مشروعي" وتسميته "نسخة من مشروعي"، إذ سيتعين عليك إذا واصلت إجراء التغييرات إنشاء نسخة أخرى تسميها "نسخة 2 من مشروعي"، ثم نسخة "نسخة 3 من مشروعي" ثم "نسخة 3b من مشروعي" ثم "نسخة من مشروعي يوم الثلاثاء" وما إلى ذلك وهلم جرًّا، وقد يكون نسخ المجلدات أمرًا بسيطًا، إلا أن هذه الطريقة تصبح أقل وأقل فاعلية مع تكرارها. بدلًا من ذلك، تعلّم استخدام نظام التحكم في الإصدار، وسيوفر لك الوقت والصداع على المدى الطويل. تعدّ كل من غيت Git و Mercurial وSubversion تطبيقات شائعة للتحكم في الإصدار، إلا أن غيت هو الأكثر شيوعًا. ستتعلم في هذه المقالة كيفية إعداد الملفات لمشاريع الشيفرات البرمجية واستخدام غيت لتتبع تغيراتها. إيداعات غيت Git يتيح لك غيت Git حفظ حالة ملفات مشروعك، المسماة لقطات snapshots أو إيداعات commits، أثناء إجراء التغييرات عليها. يمكنك بهذه الطريقة العودة إلى أي لقطة سابقة إذا احتجت إلى ذلك. قد يكون الإيداع اسمًا أو فعلًا بحسب السياق، إذ يودع المبرمجون (أو يحفظون) إيداعاتهم (أو لقطاتهم)، والمصطلح الآخر للإيداع هو تسجيل الوصول check-in إلا أنه أقل شيوعًا. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تُسهّل أنظمة التحكم في الإصدار أيضًا على فريق مطوري البرامج أن يبقوا متزامنين مع بعضهم بعضًا أثناء إجراء تغييرات على الشيفرات المشروع المصدرية، إذ يمكن للمبرمجين عندما يُجري مبرمج آخر تغييرًا ما سحب هذه التحديثات إلى حواسيبهم. يتتبع نظام التحكم في الإصدار الإيداعات الحاصلة، ومن أجراها، ومتى أجراها، جنبًا إلى جنب مع تعليقات المطورين التي تصف التعديلات. يدير نظام التحكم في الإصدار الشيفرة المصدرية للمشروع في مجلد يسمى المستودع repository -أو اختصارًا repo- ويتوجب عليك عمومًا الاحتفاظ بمستودع غيت منفصل لكل مشروع تعمل عليه. تفترض هذه المقالة أنك تعمل غالبًا بمفردك ولا تحتاج إلى ميزات غيت المتقدمة، مثل التفرع والدمج، التي تساعد المبرمجين على التعاون، ولكن حتى لو كنت تعمل بمفردك، فليس هناك مشروع برمجة صغير جدًا للاستفادة من برنامج التحكم في الإصدار. استخدام أداة Cookiecutter لإنشاء مشاريع بايثون جديدة نسمي المجلد الذي يحتوي على الشيفرة المصدرية والوثائق والاختبارات والملفات الأخرى المتعلقة بالمشروع باسم "دليل العمل" أو "شجرة العمل working tree" في أداة غيت، أو ملف المشروع عمومًا. تسمى الملفات الموجودة في دليل العمل ككل بنسخة العمل. لننشئ قبل إنشاء مستودع غيت الملفات الخاصة بمشروع بايثون. كل مبرمج لديه طريقة مفضلة لإنشاء الملفات، ومع ذلك، تتبع مشاريع بايثون اصطلاحات أسماء المجلدات والتسلسلات الهرمية. قد تتكون برامجك الأبسط من ملف ".py" واحد، ولكن عندما تتعامل مع مشاريع أكثر تعقيدًا، ستبدأ بتضمين ملفات ".py" وملفات بيانات وتوثيق واختبارات للوحدات والمزيد. يحتوي عادةً جذر مجلد المشروع على مجلد باسم src لملفات التعليمات البرمجية المصدرية ".py" ومجلد اختبارات لاختبارات الوحدات ومجلد مستندات لأي وثائق، مثل تلك التي تُنشأ بواسطة أداة التوثيق سفينكس Sphinx، بينما تحتوي الملفات الأخرى على معلومات المشروع وأداة الضبط على النحو التالي: ملف README.md للحصول على معلومات عامة، وملف .coveragerc لتغطية أداة ضبط الشيفرة، و LICENSE.txt لترخيص برنامج المشروع، وما إلى ذلك. هذه الأدوات والملفات خارج نطاق هذه السلسلة، لكنها جديرة بالبحث عنها والتعرف إليها. تصبح عملية إعادة إنشاء الملفات الأساسية السابقة ذاتها لمشاريع البرمجة الجديدة أمرًا شاقًا مع ممارستك للبرمجة لوقتٍ أطول، ويمكنك لتسريع عملية البرمجة استخدام وحدة cookiecutter الخاصة ببايثون لإنشاء هذه الملفات والمجلدات تلقائيًا، إليك التوثيق الكامل لكل من الوحدة وبرنامج سطر الأوامر Cookiecutter. لتثبيت Cookiecutter، نفذ الأمر التالي على ويندوز: pip install --user cookiecutter أو الأمر التالي على على ماك macOS ولينكس Linux: pip3 install --user cookiecutter يتضمن هذا التثبيت برنامج سطر أوامر Cookiecutter ووحدة cookiecutter الخاصة ببايثون. قد يحذرك الخرج من تثبيت برنامج سطر الأوامر في مجلد غير مدرج في متغير PATH كما يلي: Installing collected packages: cookiecutter WARNING: The script cookiecutter.exe is installed in 'C:\Users\Al\AppData\Roaming\Python\Python38\Scripts' which is not on PATH. Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location. تذكّر إضافة المجلد (C:\Users\Al...\Scripts في هذه الحالة كما في الشيفرة السابقة) إلى متغير بيئة PATH. وإلا فسيتعين عليك تنفيذ Cookiecutter مثل وحدة بايثون عن طريق إدخال ملف تعريف الارتباط python -m في ويندوز، أو ملف تعريف الارتباط python3 -m في نظامي ماك أوإس ولينكس بدلًا من الاكتفاء بكتابة cookiecutter. سنُنشئ في هذه المقالة مستودعًا لوحدة تسمى wizcoin، وهي وحدة تتعامل مع العملات المعدنية من نوع galleon، و sickle و knut وهي عملات خيالية لعالم سحري. تستخدم وحدة cookiecutter قوالب لإنشاء ملفات البداية لعدة أنواع مختلفة من المشاريع. يكون القالب غالبًا مجرد رابط لموقع غيت هب GitHub.com. على سبيل المثال، يمكنك من مجلد "C:\Users\Al" إدخال ما يلي في الطرفية Terminal لإنشاء مجلد "C:\Users\Al\wizcoin" مع الملفات الاعتيادية لمشروع بايثون الأساسي. تحمّل وحدة cookiecutter بدورها القالب من غيت هب GitHub وتسألك سلسلةً من الأسئلة حول المشروع الذي تريد إنشاءه: C:\Users\Al>cookiecutter gh:asweigart/cookiecutter-basicpythonproject project_name [Basic Python Project]: WizCoin module_name [basicpythonproject]: wizcoin author_name [Susie Softwaredeveloper]: Al Sweigart author_email [susie@example.com]: al@inventwithpython.com github_username [susieexample]: asweigart project_version [0.1.0]: project_short_description [A basic Python project.]: A Python module to represent the galleon, sickle, and knut coins of wizard currency. يمكنك أيضًا تنفيذ python -m cookiecutter إذا حصلت على خطأ بدلًا من cookiecutter، إذ يحمّل هذا الأمر نموذجًا مُنشأ من cookiecutter-basicpythonproject، وستجد قوالبًا للعديد من لغات البرمجة على الرابط، ونظرًا لاستضافة قوالب Cookiecutter غالبًا على غيت هب، فيمكنك أيضًا كتابة ":gh" اختصارًا للرابط https://github.com في سطر الأوامر. عندما يسألك Cookiecutter أسئلة، يمكنك إما إدخال إجابة أو ببساطة الضغط على مفتاح الإدخال ENTER لاستخدام الإجابة الافتراضية الموضحة بين قوسين مربعين. على سبيل المثال، يطلب منك project_name [Basic Python Project]: تسمية مشروعك، فإذا لم تدخل شيئًا سيستخدم Cookiecutter النص "Basic Python Project" اسمًا للمشروع، كما تساعدك هذه الإعدادات الافتراضية أيضًا لمعرفة نوع الإجابة المتوقعة. يعرض ما يلي اسم المشروع بأحرف كبيرة يتضمن مسافات: project_name [Basic Python Project]: بينما يوضح اسم الوحدة بأحرف صغيرة ولا تحتوي على مسافات: module_name [basicpythonproject]: لم ندخل ردًا لموجّه project_version [0.1.0]:، لذا فإن الإستجابة الافتراضية هي "0.1.0". يُنشئ Cookiecutter بعد الإجابة على الأسئلة مجلد wizcoin في مجلد العمل الحالي مع الملفات الأساسية التي ستحتاجها لمشروع بايثون، كما هو موضح في الشكل 1. [الشكل 1: الملفات الموجودة في مجلد wizcoin الذي أُنشئ بواسطة Cookiecutter.] لا بأس إذا كنت لا تفهم الغرض من هذه الملفات. الشرح الكامل لكل منها خارج نطاق هذه السلسلة، ولكن يحتوي الرابط على روابط وشرح وصفي لمزيد من القراءة. الآن وبعد أن أصبح لدينا ملفات البداية، دعنا نتتبعها باستخدام غيت. تثبيت غيت git قد يكون غيت git مثبتًا فعلًا على حاسوبك، وللتيقن من ذلك، نفّذ git --version من سطر الأوامر، فإذا رأيت رسالةً، مثل git version 2.29.0.windows.1، فهذا يعني أن غيت مثبتًا فعلًا، أما إذا رأيت رسالة الخطأ "الأمر غير موجود"، فيجب عليك تثبيت غيت. إذا كنت تستخدم نظام ويندوز فانتقل إلى "https://git-scm.com/download"، ثم حمّل مثبّت غيت Git installer وشغّله، أما إذا كنت تستخدم نظام التشغيل ماك macOS Mavericks (10.9) أو إصدارًا أحدث، فما عليك سوى تنفيذ git --version من الطرفية وستبدأ عملية تثبيت غيت في حال عدم وجوده، كما هو موضح في الشكل 2. أما إذا كنت تستخدم أوبنتو لينكس Ubuntu أو ديبيان لينكس Debian نفذ sudo apt install git-all من الطرفية Terminal، أو إذا كنت تستخدم ريدهات لينكس Red Hat، فنفذ sudo dnf install git-all من الطرفية، وإذا كنتَ تستخدم نظامًا آخر ابحث عن إرشادات لموزعين لينكس الآخرين على git-scm.com/download/linux، وتأكد من أن عملية التثبيت نجحت عن طريق تنفيذ git --version. [الشكل 2: سيُطلب منك في المرة الأولى التي تنفّذ فيها git --version على macOS 10.9 أو أحدث بتثبيت غيت.] ضبط اسم المستخدم والبريد الإلكتروني الخاصين بغيت ستحتاج إلى ضبط اسمك وبريدك الإلكتروني بعد تثبيت غيت بحيث تتضمن إيداعاتك معلومات المؤلف (أنت)، ولفعل ذلك، نفّذ الأمر git config من الطرفية وذلك باستخدام اسمك ومعلومات بريدك الإلكتروني كما يلي: C:\Users\Al>git config --global user.name "Al Sweigart" C:\Users\Al>git config --global user.email al@inventwithpython.com تُخزن هذه المعلومات في ملف ".gitconfig" في المجلد الرئيسي الخاص بك، مثل "C:\Users\Al" على حاسوبك الذي يعمل بنظام ويندوز. لن تحتاج أبدًا إلى تعديل هذا الملف النصي مباشرةً، بل بدلًا من ذلك، يمكنك تغييره عن طريق تنفيذ الأمر git config كما يمكنك إظهار إعدادات ضبط غيت الحالية باستخدام الأمر git config --list. ثبيت أدوات واجهة المستخدم الرسومية لغيت GUI Git تركز هذه المقالة على أداة سطر أوامر غيت، ولكن تثبيت البرنامج الذي يضيف واجهة المستخدم الرسومية لغيت يمكن أن يساعدك في المهام اليومية. يستخدم المبرمجون المحترفون الذين يعرفون سطر أوامر غيت CLI Git أدوات واجهة المستخدم الرسومية لغيت. تحتوي صفحة الويب https://git-scm.com/downloads/guis على العديد من هذه الأدوات، مثل TortoiseGit لنظام التشغيل ويندوز و GitHub Desktop لنظام التشغيل ماك و GitExtensions لنظام التشغيل لينكس. يوضح الشكل 3 كيف تضيف أداة TortoiseGit على ويندوز رموزًا إلى أيقونات مستعرض الملفات بناءً على حالتها؛ بحيث يشير الأخضر لملفات المستودع غير المعدلة، والأحمر لملفات المستودع المعدلة (أو المجلدات التي تحتوي على ملفات معدلة)، وتغيب الرموز عن الملفات التي لم يجري تعقبها. من المؤكد أن التحقق من هذه الرموز هو أكثر سهولة من إدخال الأوامر باستمرار في طرفية للحصول على هذه المعلومات، كما تضيف TortoiseGit أيضًا أوامر في القائمة المنسدلة لتنفيذ أوامر غيت كما هو موضح في الشكل 3. يعد استخدام أدوات واجهة المستخدم الرسومية لغيت أمرًا مريحًا، إلا أنه ليس بديلًا عن تعلم أوامر سطر الأوامر الواردة في هذه المقالة. ضع في الحسبان أنك قد تحتاج يومًا ما إلى استخدام غيت على حاسوب لم تُثَبّت أدوات واجهة المستخدم الرسومية عليه. [الشكل 3: تضيف TortoiseGit لنظام التشغيل ويندوز واجهة مستخدم رسومية لتنفيذ أوامر غيت من مستعرض الملفات.] سير عمل غيت يتضمن استخدام مستودع غيت الخطوات التالية: أولًا، إنشاء مستودع غيت عن طريق تنفيذ الأمر git init أو الأمر git clone. ثانيًا، إضافة ملفات باستخدام الأمر git add <filename> لتتبع المستودع. ثالثًا، بمجرد إضافة الملفات، يمكنك إيداع التغييرات الحاصلة فيها باستخدام الأمر: git commit -am "<descriptive commit message>" وبعد ذلك أنت جاهز لإجراء التغييرات على شفرتك مجددًا. يمكنك عرض ملف التعليمات لكل من هذه الأوامر عن طريق تنفيذ git help <command>، مثل git help init أو git help add. صفحات المساعدة هذه سهلة الاستخدام والرجوع إليها على الرغم من أنها مملة وتقنية لاستخدامها مثل وسيلة تعليمية، وستتعرف على مزيد من التفاصيل بخصوص كل من هذه الأوامر لاحقًا، ولكن أولًا، تحتاج إلى فهم بعض مفاهيم غيت لتسهيل استيعاب بقية هذه المقالة. كيفية تتبع غيت لحالة الملف الملفات الموجودة في مجلد المشروع هي ملفات مُتتبعة tracked من غيت أو غير متتبعة untracked والملفات المتتبعة هي الملفات التي تُضاف وتودع في المستودع، بينما يُصنّف أي ملف آخر على أنه ملف غير متتبع. قد لا تتواجد الملفات التي لم يجري تتبعها في مجلد المشروع في مستودع غيت، بينما توجد الملفات المتتبعة بإحدى الحالات الثلاث: حالة الإيداع committed state: هي عندما يكون الملف بنسخة مجلد المشروع مطابقًا لأحدث إيداع في المستودع، وتسمى هذه الحالة أحيانًا بالحالة غير المعدلة unmodified state أو بالحالة النظيفة clean state. الحالة المعدلة modified state: هي الحالة التي يكون عندها الملف في مجلد المشروع مختلفًا عن أحدث إيداع في المستودع. الحالة المُدرجة staged state: هي عندما يُعدّل الملف وتوضع علامة عليه ليُضمَّن في الإيداع التالي، ونقول عندها أن الملف مدرج أو في منطقة الإدراج، وتُعرف منطقة الإدراج أيضًا بالفهرس index أو ذاكرة التخزين المؤقتة cache. يحتوي الشكل 4 على رسم تخطيطي لكيفية تنقّل الملف بين هذه الحالات الأربع، ويمكنك إضافة ملف لم يُتتبَّع إلى مستودع غيت، وفي هذه الحالة يُتتبَّع ويُدرج، ومن ثم يمكنك إيداع ملفات مُدرجة لوضعها في حالة الإيداع. لا تحتاج إلى أي أمر غيت لوضع الملف في الحالة الُمعدّلة؛ فبمجرد إجراء تغييرات على ملف مودع، يُصنَّف تلقائيًا على أنه في الحالة المعدلة. [الشكل 4: الحالات المحتملة لملف في مستودع غيت والتنقل بينها.] نفذ git status في أي خطوة بعد إنشاء المستودع لعرض حالة المستودع الحالية وحالة ملفاته. ستُنفذ هذا الأمر بصورةٍ متكررة أثناء عملك مع غيت. في المثال التالي، أعددت ملفات في حالات مختلفة، لاحظ كيف تظهر هذه الملفات الأربعة في خرج git status: C:\Users\Al\ExampleRepo>git status On branch master Changes to be committed: (use "git restore --staged <file>..." to unstage) 1 new file: new_file.py 2 modified: staged_file.py Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) 3 modified: modified_file.py Untracked files: (use "git add <file>..." to include in what will be committed) 4 untracked_file.py في مشروعنا هذا، يوجد new_file.py (سطر 1) الذي أُضيفَ مؤخرًا إلى المستودع وبالتالي فهو في الحالة المُدرجة. هناك أيضًا ملفان متتبعان، وهما staged_file.py (سطر 2) و modified_file.py (سطر 3)، وهما في الحالة المُدرجة والمعدلة، على التوالي، ثم هناك ملف غير مُتتبع اسمه untracked_file.py (سطر 4). يحتوي خرج git status أيضًا على تذكيرات لأوامر غيت التي تنقل الملفات إلى حالات أخرى. ما هي الفائدة من وضع الملفات في الحالة المدرجة؟ قد تتساءل ما هو الهدف من الحالة المُدرجة، لمَ لا ننتقل فقط بين التعديل والإيداع دون الملفات المُدرجة؟ التعامل مع الحالة المُدرجة مليء بالحالات الخاصة الشائكة ومصدر كبير للارتباك للمبتدئين في غيت. على سبيل المثال، يمكن تعديل الملف بعد إدراجه، مما يؤدي إلى وجود ملفات في كل من الحالات المعدلة والمُدرجة، كما هو موضح في القسم السابق. تقنيًا، لا تحتوي المنطقة المُدرجة على ملفات بقدر ما تحتوي على تغييرات، لأنه يمكن لأجزاء من ملف واحد معدل أن تكون في الحالة المدرجة، بينما لا تنتمي أجزاء أخرى لهذه الحالة. هذه هي الحالات التي تسببت بشهرة غيت إذ أنها معقدة، وغالبًا ما تكون العديد من مصادر المعلومات حول كيفية عمل غيت غير دقيقة في أحسن الأحوال ومضللة في أسوأ الأحوال. إنشاء مستودع غيت على حاسوبك غيت هو نظام تحكم في الإصدار الموزع، مما يعني أنه يخزن جميع اللقطات وبيانات المستودع الوصفية محليًا على حاسبك في مجلد يسمى ".git". على عكس نظام التحكم في الإصدار المركزي، لا يحتاج غيت للاتصال بخادم عبر الإنترنت لإجراء الإيداعات، مما يجعل غيت سريعًا ومتاحًا للعمل معه عندما تكون غير متصل بالإنترنت. من الطرفية، نفذ الأوامر التالية لإنشاء مجلد ".git". ستحتاج في نظامي ماك أو إس ولينكس، إلى تنفيذ mkdir بدلًا من md. C:\Users\Al>md wizcoin C:\Users\Al>cd wizcoin C:\Users\Al\wizcoin>git init Initialized empty Git repository in C:/Users/Al/wizcoin/.git/ عند تحويل مجلد إلى مستودع غيت بتنفيذ git init، تبدأ جميع الملفات الموجودة فيه بدون تتبع. بالنسبة لمجلد wizcoin الخاص بنا، يُنشئ الأمر git init مجلدًا يدعى "wizcoin/.git"، الذي يحتوي بدوره على البيانات الوصفية لمستودع غيت. يؤدي وجود هذا المجلد ".git" إلى جعل المجلد مستودع غيت؛ وبدونه سيكون لديك ببساطة مجموعة من ملفات الشيفرة المصدرية في مجلد عادي. لن تضطر أبدًا إلى تعديل الملفات في ".git" مباشرةً، لذا تجاهل هذا المجلد. في الواقع، سُميَ المجلد ".git" بهذا الاسم لأن معظم أنظمة التشغيل تخفي تلقائيًا المجلدات والملفات التي تبدأ أسماؤها بنقطة. الآن لديك مستودع في المجلد "C:\Users\Al\wizcoin". يُعرف المستودع الموجود على حاسبك باسم المستودع المحلي local repo؛ يُعرف المستودع الموجود على حاسوب شخص آخر باسم المستودع البعيد remote repo. هذا التمييز مهم، لأنه سيتعين عليك غالبًا مشاركة الإيداعات بين المستودعات البعيدة والمحلية حتى تتمكن من العمل مع مطورين آخرين في المشروع ذاته. يمكنك الآن استخدام الأمر git لإضافة ملفات وتتبع التغييرات داخل مجلد المشروع، إذ سترى ما يلي إذا نفذت git status في المستودع الذي أنشأته حديثًا: C:\Users\Al\wizcoin>git status On branch master No commits yet nothing to commit (create/copy files and use "git add" to track) يخبرك خرج هذا الأمر أنه ليس لديك أي إيداعات حتى الآن في هذا المستودع. تنفيذ أمر git status مع أمر watch أثناء استخدام أداة سطر أوامر غيت، ستنفّذ غالبًا الأمر git status لمعرفة حالة المستودع الخاص بك، وبدلًا من إدخال هذا الأمر يدويًا، يمكنك استخدام الأمر watch لتنفيذه نيابةً عنك. ينفّذ الأمر watch أمرًا معينًا بصورةٍ متكررة كل ثانيتين، مع تحديث الشاشة بأحدث خرج لها. يمكنك الحصول على أمر watch عن طريق تنزيل InventWithPython في نظام التشغيل ويندوز، أو يمكنك الحصول على أمر watch عن طريق تنزيل InventWithPython ووضع هذا الملف في مجلد "PATH"، مثل "C:\Windows"، بينما يمكنك الانتقال إلى https://www.macports.org/ في نظام ماك، لتنزيل MacPorts وتثبيته، ثم نفذ sudo ports install watch. يحتوي نظام لينكس على هذا الأمر فعلًا، وبمجرد تثبيته، افتح موجه أوامر جديد أو نافذة طرفية جديدة، ونفذ cd لتغيير المجلد إلى مجلد مستودع غيت الخاص بك، ونفذ watch "git status"؛ إذ سيعمل الأمر watch على تنفيذ git status كل ثانيتين، ويعرض أحدث النتائج على الشاشة. يمكنك ترك هذه النافذة مفتوحة أثناء استخدام أداة سطر أوامر غيت في نافذة طرفية مختلفة لترى كيف تتغير حالة المستودع في الوقت الفعلي، كما يمكنك فتح نافذة طرفية أخرى وتنفيذ watch "git log -oneline" لعرض ملخص الإيداعات التي تفعلها، والتي يجري تحديثها أيضًا في الوقت الفعلي. تساعد هذه المعلومات في إزالة الغموض المتعلق بما تفعله أوامر غيت التي تكتبها في المستودع الخاص بك. إضافة ملفات لغيت لتعقبها يمكن فقط إيداع أو التراجع عن أو التفاعل مع الملفات المتتبعة من خلال الأمر git. نفّذ git status لمعرفة حالة الملفات في مجلد المشروع: C:\Users\Al\wizcoin>git status On branch master No commits yet 1 Untracked files: (use "git add <file>..." to include in what will be committed) .coveragerc .gitignore LICENSE.txt README.md --snip-- tox.ini nothing added to commit but untracked files present (use "git add" to track) لم يحدث تعقب لأي من الملفات الموجودة في مجلد "wizcoin" حاليًا (سطر 1)، ويمكننا تتبعها عن طريق إجراء إيداع أولي لهذه الملفات، الذي يكون على خطوتين: تنفيذ git add لكل ملف يجري إيداعه، ثم تنفيذ git commit لإنشاء إيداع لكل هذه الملفات، ويتتبع غيت الملف بمجرّد إيداعه. ينقل الأمر git add الملفات من حالة عدم التتبع أو الحالة المعدلة إلى الحالة المُدرجة، إذ يمكننا تنفيذ git add لكل ملف نخطط لتعديله. على سبيل المثال، git add .coveragerc و git add .gitignore و git add LICENSE.txt وما إلى ذلك، لكن هذا أمر ممل. بدلًا من ذلك، دعنا نستخدم الرمز * لإضافة عدة ملفات مرة واحدة. على سبيل المثال، يضيف git add *.py جميع ملفات ".py" في مجلد العمل الحالي والمجلدات الفرعية الخاصة به. لإضافة كل ملف لم يجري تعقبه، استخدم نقطة واحدة (.) لإخبار غيت بمطابقة جميع الملفات: C:\Users\Al\wizcoin>git add . نفّذ git status لرؤية الملفات التي أدرجتها: C:\Users\Al\wizcoin>git status On branch master No commits yet 1 Changes to be committed: (use "git rm --cached <file>..." to unstage) 2 new file: .coveragerc new file: .gitignore --snip-- new file: tox.ini يخبرك خرج git status عن الملفات التي ستُنفَّذ على مراحل في المرة التالية التي تنفّذ فيها git commit (سطر 1)، ويخبرك أيضًا أن هذه ملفات جديدة أُضيفت إلى المستودع (سطر 2) بدلًا من الملفات المُعدّلة الموجودة في المستودع. بعد تنفيذ git add لتحديد الملفات المراد إضافتها إلى المستودع، نفذ الأمر التالي: git commit -m "Adding new files to the repo.” و git status مرةً أخرى لعرض حالة المستودع: C:\Users\Al\wizcoin>git commit -m "Adding new files to the repo." [master (root-commit) 65f3b4d] Adding new files to the repo. 15 files changed, 597 insertions(+) create mode 100644 .coveragerc create mode 100644 .gitignore --snip-- create mode 100644 tox.ini C:\Users\Al\wizcoin>git status On branch master nothing to commit, working tree clean لاحظ أن أي ملفات مدرجة في ملف ".gitignore" لن تُضاف إلى منطقة الإدراج، كما سيوضح القسم التالي. تجاهل الملفات في المستودع تظهر الملفات التي لم يجري تتبعها بواسطة غيت على أنها لم غير متتبعة عند تنفيذ git status، ولكن قد ترغب في استبعاد ملفات معينة من نظام التحكم في الإصدار تمامًا أثناء كتابة الشيفرة البرمجية، وذلك حتى لا تتبعها عن طريق الخطأ. وتشمل هذه الملفات: الملفات المؤقتة في مجلد المشروع. ملفات ".pyc" و ".pyo" و ".pyd" التي ينشئها مُفسر بايثون عند تنفيذ برامج ".py". ملفات ".tox" و "htmlcov" والمجلدات الأخرى التي تنشئها أدوات تطوير البرامج المختلفة docs/_build. أي ملفات أخرى مجمعة أو مُنشأة يمكن إعادة إنشائها (لأن المستودع مخصص للملفات المصدرية، وليس للمنتجات المُنشأة من الملفات المصدرية). ملفات الشيفرة المصدرية التي تحتوي على كلمات مرور قاعدة البيانات أو رموز المصادقة المميزة أو أرقام بطاقات الائتمان أو غيرها من المعلومات الحساسة. لتجنب تضمين هذه الملفات، أنشئ ملفًا نصيًا باسم ".gitignore" يحتوي على المجلدات والملفات التي يجب ألا يتتبعها غيت مطلقًا، وسيستثني غيت بدوره هذه الملفات والمجلدات تلقائيًا من أوامر git add أو git commit، ولن تظهر عند تنفيذ git status. يبدو ملف ".gitignore" الذي ينشئه قالب cookiecutter-basicpythonproject على النحو التالي: # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class --snip-- يستخدم ملف ".gitignore" الرمز * لأحرف البدل wildcards و # للتعليقات. يمكنك قراءة المزيد عنها على صفحة gitignore في التوثيق الرسمي. يجب عليك إضافة ملف ".gitignore" الفعلي إلى مستودع غيت حتى يتمكن المبرمجون الآخرون من الحصول عليه إذا استنسخوا clone مستودعك. إذا كنت تريد معرفة أي الملفات في دليل العمل قد جرى تجاهلها بناءً على الإعدادات في .gitignore، فنفذ الأمر التالي: git ls-files --other --ignored --exclude-standard الخلاصة أنظمة التحكم في الإصدار منقذة المبرمجين، إذ يؤدي إيداع لقطات من الشيفرة البرمجية إلى تسهيل مراجعة تقدمك، وفي بعض الحالات، التراجع عن التغييرات التي لا تحتاج إليها. من المؤكد أن تعلُّم أساسيات نظام التحكم في الإصدار مثل غيت يوفر لك الوقت على المدى الطويل. تحتوي مشاريع بيثون عادةً على العديد من الملفات والمجلدات الافتراضية، وتساعدك وحدة cookiecutter في إنشاء نموذج أساسي للعديد من هذه الملفات. تشكل هذه الملفات الملفات الأولى التي تودعها في مستودع غيت المحلي الخاص بك. نسمي المجلد الذي يحتوي على كل هذا المحتوى مجلد العمل أو مجلد المشروع. ترجمة -وبتصرف- لقسم من الفصل ORGANIZING YOUR CODE PROJECTS WITH GIT من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: تلميحات النوع Type Hints في بايثون بدء العمل مع نظام إدارة الإصدارات جيت Git مبادئ Git الأساسية
-
تلميحات الأنواع هي موجّهات directives يمكن إضافتها في الشيفرة المصدرية لبايثون لتحديد أنواع البيانات للمتغيرات والمعاملات والقيم المُعادة، وهذا يسمح لأدوات تحليل الشيفرة الساكنة بالتأكد من أن الشيفرة الخاصة بك لا تُتسبب بأي استثناءات exceptions بسبب قيم ذات نوع خاطئ. ظهرت تلميحات الأنواع أول مرة في إصدار بايثون 3.5 ولكن بما أنها مبنية على التعليقات فيمكن استخدامها مع أي إصدار بايثون. ما هي تلميحات النوع؟ معظم اللغات البرمجية صارمة بضبط أنواع البيانات وتسمى ثابتة الأنواع static typing أي أنه يجب على المبرمجين تحديد أنواع البيانات صراحةً لكل من المتغيرات والمُعاملات والقيم المُعادة في الشيفرة المصدرية، مما يسمح للمفسّر أو المصرّف التحقق من أن الشيفرة تستخدم كل الكائنات بصورةٍ صحيحة قبل تنفيذ البرنامج. على الجانب الآخر، هنالك لغات برمجة -ومنها لغة بايثون- متساهلة في تحديد أنواع البيانات وتُسمى متغيرة أو ديناميكية الأنواع dynamic typing أي يُمكن أن تكون المتغيرات والمُعاملات والقيم المُعادة من أي نوع من البيانات ويمكن أن تغير نوعها عند تنفيذ البرنامج. تكون اللغات الديناميكية غالبًا أكثر سهولة للبرمجة عليها لأنها لا تحتاج كثيرًا إلى تحديد صريح لأنواع البيانات، إلا أنها تفتقد ميزات منع الأخطاء التي تكون موجودة في اللغات ثابتة الأنواع. عندما تكتب سطرًا من شيفرة بايثون مثل round('forty two') لا تلاحظ أنك تمرر سلسلة نصية إلى دالة تقبل نوع البيانات عدد صحيح int أو عدد عشري float فقط حتى تُنفذ الشيفرة وتسبب خطأ، بينما تعطي اللغات ثابتة الأنواع تحذيرًا مسبقًا إذا أردت تعيين قيمة أو تمرير وسيط من النوع الخاطئ. تقدم تلميحات الأنواع الخاصة بلغة بايثون خيار كتابة الشيفرة بأنواع ثابتة، وتكون التلميحات بالخط الغامق كما في المثال التالي: def describeNumber(number: int) -> str: if number % 2 == 1: return 'An odd number. ' elif number == 42: return 'The answer. ' else: return 'Yes, that is a number. ' myLuckyNumber: int = 42 print(describeNumber(myLuckyNumber)) تستخدم تلميحات الأنواع النقطتين في المُعاملات والمتغيرات لفصل الاسم عن النوع ولكن بالنسبة للقيم المُرجعة يستخدم تلميح النوع السهم (->) للفصل بين أقواس تغليف تعبير def عن النوع. يلمّح نوع الدلة describeNumber() أنها تأخذ قيمة عدد صحيح من أجل مُعامل number وتعيد قيمة سلسلة نصية. لا تحتاج إلى تطبيق تلميحات النوع إذا استخدمتها في مكان ما على كل جزء من البيانات في برنامجك، لكن استخدم بدلًا من ذلك طريقة كتابة تدريجية gradual typing وهو أسلوب كتابة وسطي يمنحك مرونة الكتابة الديناميكية وأمان الكتابة الساكنة وبهذا يمكننا إضافة تلميحات الأنواع لأنواع معينة من المتغيرات والمعاملات والقيم المُرجعة، إلا أنه كلما زادت تلميحات الأنواع في برنامجك كلما زادت المعلومات التي تأخذها أدوات تحليل الشيفرة الساكنة لمتابعة المشاكل المحتملة في البرنامج. لاحظ في المثال السابق أن أسماء الأنواع المحددة تطابق أسماء الدوال البانية int() و str()، للصنف والنوع ونوع البيانات المعنى ذاته في بايثون، إذ تجد في أي نسخة مكونة من أصناف أن اسم الصنف مماثلٌ لاسم النوع: import datetime 1 noon: datetime.time = datetime.time(12, 0, 0) class CatTail: def __init__(self, length: int, color: str) -> None: self.length = length self.color = color 2 zophieTail: CatTail = CatTail(29, 'grey') لدى المتغير noon تلميح النوع datetime.time (سطر 1) لأنه كائن time (المعرف في وحدة datetime)، وللكائن zophieTail أيضًا نوع التلميح CatTail (سطر 2)، وذلك لأنه كائن لصنف CatTail الذي أنشأناه بتعليمة class. تُطبَّق تلميحات الأنواع تلقائيًا لكل الأصناف الفرعية للنوع المحدد، فمثلًا يمكن ضبط متغير تلميح النوع dict لأي قيمة مسار وأيضًا لأي قيم collection.OrderedDict و collection.defaultdict لأن هذه الأصناف هي أصناف فرعية للصنف dict. سنتكلم لاحقًا عن الأصناف الفرعية بتفصيل أكبر. لا تحتاج عادةً أدوات تحقق النوع الساكنة إلى تلميحات أنواع للمتغيرات، لأن أدوات التحقق من أنواع البيانات تستدلّ على النوع type inference من تعبير الإسناد الأول للمتغير. على سبيل المثال، يمكن لمتحقق النوع الاستدلال بأن spam في السطر spam = 42 يجب أن يحتوي تلميح نوع int، إلا أنه يُفضل ضبط تلميح نوع بكل الأحوال. أي تغيير مستقبلي للنوع float كما في spam = 42.0 سيسبب بتغيير في النوع المُستدلّ مما قد لا يكون متوافقًا مع رغبتك، إذ يُفضّل إجبار المبرمج ليغير تلميح النوع عند تغيير القيمة للتأكد أنه غيّر النوع متقصدًا بدلًا من تغيير عَرَضي خاطئ. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن استخدام محللات صارمة مع أنواع البيانات على الرغم من أن بايثون تدعم صياغة تلميحات الأنواع إلا أن مفسر بايثون يتجاهلها كليًّا؛ فإذا نفذت برنامج بايثون يمرر متغيرًا من نوع غير صالح للدالة، فستتصرف بايثون كأن تلميحات النوع غير موجودة. بمعنى آخر، لا تتسبب تلميحات النوع بأن يجري مفسر بايثون أي تحقق من الأنواع وقت التنفيذ، إذ أنها موجودةٌ فقط لتخدم أدوات التحقق من أنواع البيانات التي تحلل الشيفرة قبل تنفيذ البرنامج وليس أثناء تنفيذ البرنامج. نسمي هذه الأدوات بأدوات التحليل الثابتة static analyzers tools، لأنها تحلل الشيفرة المصدرية قبل تنفيذ البرنامج، بينما تحلّل أدوات التحليل وقت التنفيذ runtime analysis tools أو أدوات التحليل الديناميكي dynamic analysis tools البرامج وقت التنفيذ. قد تبدو لك الأمور مثيرة للحيرة الآن، إذ تشير الكلمتين ساكن وديناميكي إلى تنفيذ البرنامج بينما تشير الكتابة ثابتة الأنواع static typing ومتغيرة الأنواع dynamic typing إلى كيفية التصريح عن أنواع البيانات للمتغيرات والدوال. بايثون لغةٌ تُكتب ديناميكيًا ولديها أدوات تحليل ساكنة مثل Mypy مكتوبةٌ لأجلها. تثبيت وتشغيل Mypy على الرغم من أن بايثون لا تحتوي على أداة تحقق من الأنواع رسميًا، إلا أن Mypy تُعدّ أشهر أداة خارجية للتحقق من النوع، ويمكنك تثبيتها باستخدام pip عن طريق تنفيذ الأمر التالي: python -m pip install -user mypy شغل python3 بدلًا من python على ماك أو إس macOS ولينكس Linux. تتضمن أدوات التحقق من النوع الأُخرى المعروفة Pyright الخاص بمايكروسوفت و Pyre الخاص بفيسبوك و Pytype الخاص بجوجل. لتشغيل متحقق النوع، افتح سطر الأوامر أو نافذة طرفية ونفّذ أمر python -m mypy لتنفيذ الوحدة مثل تطبيق، ومرّر اسم ملف شيفرة بايثون ليتحقق منه. نتحقق في هذا المثال من الشيفرة لبرنامج مثال أُنشئ في ملف اسمه example.py: C:\Users\Al\Desktop>python –m mypy example.py Incompatible types in assignment (expression has type "float", variable has type "int") Found 1 error in 1 file (checked 1 source file) لا يخرِج متحقق النوع شيئًا إذا لم توجد أي مشكلة ويطبع رسائل خطأ عدا ذلك، ففي هذا المثال هناك مشكلة في ملف example.py في السطر 171 لأن المتغير المُسمى spam لديه تلميح نوع int ولكن تُسند إليه قيمة float، وقد يسبب هذا فشلًا ويجب التحقق منه. قد تكون بعض رسائل الخطأ صعبة الفهم للوهلة الأولى، ويمكن أن يعطي Mypy عددًا كبيرًا من الأخطاء المحتملة أكثر مما نستطيع ذكره هنا. أسهل طريقة لمعرفة ماذا يعني كل خطأ هو البحث عنه على الويب، ويمكنك البحث عن شيء شبيه بما يلي: " أنواع الإسناد غير المتوافقة في Mypy" أو Mypy incompatible types in assignment. يُعد تنفيذ Mypy من سطر الأوامر في كل مرة تغيّر فيها الشيفرة أمرًا غير فعال، وللحصول على استخدام أفضل من متحقق النوع يجب عليك ضبط بيئة التطوير المتكاملة IDE أو محرر النصوص الخاص بك ليعمل في الخلفية، فبهذه الطريقة سيبقى المحرر ينفّذ Mypy أثناء كتابة الشيفرة ويظهر أي أخطاء في المحرر. يبين الشكل 1 الأخطاء من المثال السابق في محرر النصوص Sublime Text. [الشكل 1: إظهار محرر النصوص Sublime Text للأخطاء من Mypy] تختلف خطوات ضبط محرر النصوص أو بيئة التطوير المتكاملة للعمل على Mypy اعتمادًا على أي محرر نصوص أو بيئة تطوير متكاملة مُستخدمة. يمكنك إيجاد الخطوات على الويب عن طريق البحث عن "ضبط Mypy على <اسم بيئة التطوير المتكاملة الخاصة بك>" أو "ضبط تلميحات النوع على <اسم بيئة التطوير المتكاملة الخاصة بك>" أو ما شابه، وإذا فشل كل شيء أخر يمكنك دائما تنفيذ Mypy من سطر الأوامر أو نافذة الطرفية. إعلام Mypy بتجاهل الشيفرة ربما تريد كتابة شيفرة ولا ترغب بأن ترى تحذيرات تلميح النوع لهذه الشيفرة، ولربما يظهر لأداة التحليل الساكنة أن السطر يستخدم النوع الخاطئ ولكنه يكون صالحًا وقت تنفيذ البرنامج. يمكنك تجاهل أي تحذير تلميح نوع بإضافة تعليق # type: ignore في نهاية السطر، إليك مثالًا عن ذلك: def removeThreesAndFives(number: int) -> int: number = str(number) # type: ignore number = number.replace('3', '').replace('5', '') # type: ignore return int(number) يمكننا ضبط متغير العدد الصحيح إلى سلسلة نصية مؤقتًا لإزالة كل أرقام 3 و5 من العدد الصحيح المُمرر إلى removeThreesAndFives()، يتسبب ذلك بتحذير متحقق النوع من أول سطرين من الدالة لذا سنضيف تلميح النوع # type: ignore لهذه الأسطر لتجاهل تحذيرات متحقق النوع. استخدم # type: ignore باعتدال، إذ يفتح تجاهل التحذيرات من متحقق النوع المجال للأخطاء بأن تتسلل إلى الشيفرة الخاصة بك، ويمكنك دائمًا إعادة كتابة الشيفرة الخاصة بك حتى لا تظهر هذه التحذيرات، فمثلًا إذا أنشأنا متغيرًا باستخدام numberAsStr = str(number) أو بدلنا الأسطر الثلاثة باستخدام سطر شيفرة واحد كما يلي: return int(str(number.replace('3', '').replace('5', ''))) يمكننا تجنب استخدام المتغير number في عدة أنواع. لا نريد هنا تجاهل التحذيرات عن طريق تغيير تلميح النوع للمُعامل إلى union[int, str] لأن هدف المعامل هو السماح بالأعداد الصحيحة فقط. ضبط تلميحات النوع لأنواع متعددة يمكن أن تحتوي متغيرات ومعاملات والقيم المُعادة الخاصة بلغة بايثون على العديد من أنواع البيانات، ولملائمة ذلك يمكن تحديد تلميحات الأنواع عن طريق استيراد Union من الوحدة المضمنة typing وتحديد مجال الأنواع داخل الأقواس المربعة بعد اسم الصنف Union. from typing import Union spam: Union[int, str, float] = 42 spam = 'hello' spam = 3.14 في هذا المثال، يحدد تلميح النوع Union[int, str, float] أنه يمكن ضبط spam إلى عدد صحيح أو سلسلة نصية أو رقم عشري. لاحظ أنه من الأفضل استخدام الشكل from typing import x من تعبير import بدلًا من الشكل import typing ومن ثم استخدام الكتابة المطولة باستمرار من أجل تلميحات النوع في كل البرنامج. يمكن تحديد أنواع بيانات متعددة في المواقف التي يمكن أن تعيد فيها المتغيرات أو القيم المُعادة قيمة None بالإضافة إلى نوع آخر. ضع None داخل أقواس معقوفة بدلًا من NoneType لإضافة NoneType الذي هو نوع القيمة None في تلميح النوع. تقنيًا، ليس NoneType معرّفًا مبنيًا مسبقًا built-in identifier كما هو الحال مع int أو str. الأفضل من ذلك، بدلًا من استخدام Union[str, None] على سبيل المثال، يمكنك استيراد Optional من الوحدة Optional[str]؛ يعني تلميح النوع هذا أن التابع أو الدالة قد تُعيدان None بدلًا من القيمة للنوع المتوقع، إليك مثالًا عن ذلك: from typing import Optional lastName: Optional[str] = None lastName = 'Sweigart' يمكن ضبط المتغير lastName في هذا المثال إلى قيمة None أو str، ولكن من الأفضل التقليل من استخدام Union و Optimal، إذ كلما قل عدد الأنواع التي تسمح بها المتغيرات والدوال كلما كانت الشيفرة أبسط، والشيفرة البسيطة أقل عرضةً للأخطاء من الشيفرة المعقدة. تذكر الحكمة الفضلى في بايثون أن البسيط أفضل من المعقد، فمن أجل الدوال التي تُعيد None للإشارة إلى خطأ ما، جرّب استخدام استثناء بدلًا من ذلك. يمكنك استخدام تلميح النوع Any (أيضًا من وحدة typing) لتحديد أن المتغير أو المعامل أو القيمة المُعادة يمكن أن تكون من أي نوع بيانات: from typing import Any import datetime spam: Any = 42 spam = datetime.date.today() spam = True يسمح تلميح النوع Any في هذا المثال بضبط المتغير spam إلى قيمة من أي نوع من البيانات، مثل int أو datetime.date أو bool، كما يمكنك أيضًا استخدام object مثل تلميح نوع لأن هذا هو الصنف الأساسي لكل أنواع البيانات في بايثون، ولكن Any هو تلميح مفهوم أكثر من object. استخدم Any باعتدال كما هو الأمر مع Union و Optional، إذ أنك ستفقد مزايا التحقق من النوع إذا ضبطت كل المتغيرات والمعاملات والقيم المرجعة الخاصة بك إلى نوع التلميح Any. الفرق بين تحديد تلميح النوع Any وعدم تحديد أي تلميح نوع هو أن Any توضح صراحةً أن المتغير أو التابع يقبل القيم من أي نوع، بينما يشير غياب تلميح النوع إلى أن المتغير أو التابع لم يُلمَّح لنوعه بعد. ضبط تلميحات النوع لكل من القوائم والقواميس وغيرها يمكن للقوائم lists والقواميس dictionaries والصفوف tuples والمجموعات sets وحاويات البيانات الأخرى أن تحتفظ بقيم أخرى، فإذا حددت list على أنها تلميح النوع للمتغير، يجب على هذا المتغير أن يحتوي على قائمة، إلا أنه من الممكن أن تحتوي تلك القائمة على قيمة من أي نوع. لا تسبب الشيفرة التالية أي اعتراضات من متحقق النوع: spam: list = [42, 'hello', 3.14, True] يجب استخدام تلميح النوع List الخاص بالوحدة typing للتصريح بالتحديد عن أنواع البيانات داخل القائمة. لاحظ أن List مكتوبةٌ بحرف كبير "L" لتمييزها عن نوع البيانات list: from typing import List, Union 1 catNames: List[str] = ['Zophie', 'Simon', 'Pooka', 'Theodore'] 2 numbers: List[Union[int, float]] = [42, 3.14, 99.9, 86] يحتوي المتغير catNames في هذا المثال على قائمة من السلاسل النصية، لذا نضبط بعد استيراد List من الوحدة typing تلميح النوع إلى List[str] (السطر 1)، وينظر متحقق النوع إلى أي استدعاء للتابعين append() أو insert() أو أي شيفرة أخرى تضع قيمة غير السلسلة النصية في القائمة. يمكننا ضبط تلميح النوع باستخدام Union إذا احتوت القائمة على أنواع متعددة، إذ يمكن مثلًا للقائمة numbers أن تحتوي على قيم ذات عدد صحيح أو عدد عشري، لذا نضبط تلميح النوع إلى List[Union[int, float]] (السطر 2). لدى وحدة typing اسم بديل آخر لكل نوع حاوية، إليك قائمةً بأسماء الأنواع البديلة لأنواع الحاويات المعروفة في بايثون: List هي لنوع البيانات list. Tuple هي لنوع البيانات tuple. Dict هي لنوع البيانات القاموس dict. Set هي لنوع البيانات set. FrozenSet هي لنوع البيانات frozenset. Sequence هي لنوع البيانات list و tuple وأي نوع آخر من أنواع البيانات المتسلسلة. Mapping هي لنوع البيانات القاموس dict و set و frozenset وأي نوع آخر من أنواع بيانات الربط. ByteString هي لأنواع bytes و bytearray و memoryview. إليك قائمة كاملة للأنواع. النقل العكسي لتلميحات النوع باستخدام التعليقات النقل العكسي Backporting هي عملية أخذ الميزات من إصدار جديد من البرنامج ونقلها (أي تكييفها وإضافتها) إلى نسخة أقدم. ميزة تلميحات النوع لبايثون جديدةٌ للإصدار 3.5 ولكن يمكن لشيفرة بايثون التي تُنفذ بإصدار مفسر أقدم من 3.5 استخدام تلميحات النوع بوضع معلومات النوع في التعليقات. استخدم التعليق السطري بعد تعبير الإسناد من أجل المتغيرات، ومن أجل الدوال والتوابع اكتب تلميح النوع على السطر الذي يلي تعبير def. ابدأ بالتعليق باستخدام type متبوعًا بنوع البيانات. إليك مثالًا عن شيفرة تحتوي على تلميحات النوع في التعليقات: 1 from typing import List 2 spam = 42 # type: int def sayHello(): 3 # type: () -> None """The docstring comes after the type hint comment.""" print('Hello!') def addTwoNumbers(listOfNumbers, doubleTheSum): 4 # type: (List[float], bool) -> float total = listOfNumbers[0] + listOfNumbers[1] if doubleTheSum: total *= 2 return total لاحظ أنه حتى مع استخدامك لتنسيق التعليق الخاص بتلميح النوه فأنت لا تزال بحاجة لاستيراد الوحدة typing (سطر 1)، إضافةً لأي نوع اسم بديل تستخدمه في التعليقات. لا تحتوي الإصدارات الأقدم من 3.5 وحدة typing في مكتبتها الافتراضية، لذا عليك تثبيت typing بصورةٍ منفصلة عن طريق تنفيذ هذا الأمر: python -m pip install --user typing شغل python3 بدلًا من python في ماك أو إس ولينكس. لضبط المتغير spam إلى عدد صحيح نضيف # type: int مثل تعليق بنهاية السطر (سطر 2). من أجل الدوال يجب أن يضم التعليق قوسين مع فاصلة تفصل قتئمة تلميح الأنواع بنفس ترتيب المعاملات. يجب أن يكون للدوال التي تحتوي على صفر معامل قوسين فارغين (سطر 3)، كما يجب الفصل بين المعاملات المتعددة إذا وجدت داخل القوسين بفواصل (سطر 4). تنسيق التعليق ذي تلميح النوع أصعب للقراءة من التنسيق العادي، لذا استخدمه فقط من أجل الشيفرة التي تنفذها إصدارات بايثون أقدم من 3.5. ترجمة -وبتصرف- لقسم من الفصل COMMENTS, DOCSTRINGS, AND TYPE HINTS من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: التعليقات Comments وأنواعها في لغة بايثون أساسيات البرمجة بلغة بايثون الدليل السريع إلى لغة البرمجة بايثون
-
بعد أن تطرّقنا إلى كيفية التعامل مع أداة غيت ومفاهيمه الأساسية وأهمية استخدامه في مشاريع بايثون والمشاريع البرمجة عمومًا في المقالين السابقين فهم نظام التحكم بالإصدارات Git وأهمية استخدامه في مشاريع بايثون ومقال حفظ التغييرات وعرضها باستخدام غيت Git في مشاريع بايثون، نتابع في هذا المقال بعض الأمور الأكثر تقدمًا في غيت مثل التراجع عن الإيداع وإيجاد الفرق بين ملفات الإصدارات المختلفة ورفع شيفرة مشروعك المصدرية إلى مستودع بعيد على الإنترنت باستخدام منصة غيت هب GitHub. حذف ملفات من مستودع إذا لم تعد بحاجة لغيت لتتبع الملف، فلا يمكنك ببساطة حذف الملف من نظام الملفات، إذ يجب عليك حذفه من خلال غيت باستخدام الأمر git rm، الذي يخبر غيت أيضًا بإلغاء تتبع الملف. للتمرن على ذلك، نفذ الأمر التالي لإنشاء ملف صغير يسمى deleteme.txt يحتوي النص "Test file": echo "Test file"> deleteme.txt ثم أودعه في المستودع عن طريق تنفيذ الأوامر التالية: C:\Users\Al\wizcoin>echo "Test file" > deleteme.txt C:\Users\Al\wizcoin>git add deleteme.txt C:\Users\Al\wizcoin>git commit -m "Adding a file to test Git deletion." [master 441556a] Adding a file to test Git deletion. 1 file changed, 1 insertion(+) create mode 100644 deleteme.txt C:\Users\Al\wizcoin>git status On branch master nothing to commit, working tree clean لا تحذف الملف باستخدام الأمر del في نظام التشغيل ويندوز، أو الأمر rm في نظامي ماك macOS ولينكس Linux، وإذا نفذت ذلك، يمكنك تنفيذ git restore <filename> لاستعادته أو ببساطة الاستمرار في الأمر git rm لإزالته من المستودع، واستخدم بدلًا من ذلك الأمر git rm لحذف ملف deleteme.txt وإدراجه كما يوضح هذا المثال: C:\Users\Al\wizcoin>git rm deleteme.txt rm deleteme.txt' يحذف الأمر git rm الملف من نسخة العمل الخاصة بك، لكنك لم تنته بعد. يُدرج الأمر git rm الملف، مثل git add، وتحتاج إلى تنفيذ حذف الملف تمامًا مثل أي تغيير آخر: C:\Users\Al\wizcoin>git status On branch master Changes to be committed: 1 (use "git reset HEAD <file>..." to unstage) deleted: deleteme.txt C:\Users\Al\wizcoin>git commit -m "Deleting deleteme.txt from the repo to finish the deletion test." [master 369de78] Deleting deleteme.txt from the repo to finish the deletion test. 1 file changed, 1 deletion(-) delete mode 100644 deleteme.txt C:\Users\Al\Desktop\wizcoin>git status On branch master nothing to commit, working tree clean على الرغم من أنك حذفت deleteme.txt من نسخة العمل الخاصة بك، إلا أنها لا تزال موجودة في محفوظات المستودع. يصف قسم "استرداد التغييرات القديمة" لاحقًا في هذه المقالة كيفية استرداد ملف محذوف أو التراجع عن تغيير. يعمل الأمر git rm فقط على الملفات الموجودة في حالة الإيداع، دون أي تعديلات. بخلاف ذلك، يطلب منك غيت إيداع التغييرات أو التراجع عنها باستخدام الأمر git reset HEAD <filename>، ويذكّرك خرج git status بهذا الأمر في السطر 1. يمنعك هذا الإجراء من حذف التغييرات غير المودعة بها عن طريق الخطأ. إعادة تسمية ونقل الملفات في المستودع على غرار حذف ملف، لا ينبغي عليك إعادة تسمية ملف موجود في المستودع أو نقله إلا إذا كنت تستخدم غيت، وإذا حاولت فعل ذلك دون استخدام غيت، سيعتقد أنك حذفت ملفًا ثم أنشأت ملفًا جديدًا يحتوي على نفس المحتوى. بدلًا من ذلك، استخدم الأمر git mv متبوعًا بالأمر git commit. دعنا نعيد تسمية الملف README.md إلى README.txt عن طريق تنفيذ الأوامر التالية: C:\Users\Al\wizcoin>git mv README.md README.txt C:\Users\Al\wizcoin>git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) renamed: README.md -> README.txt C:\Users\Al\wizcoin>git commit -m "Testing the renaming of files in Git." [master 3fee6a6] Testing the renaming of files in Git. 1 file changed, 0 insertions(+), 0 deletions(-) rename README.md => README.txt (100%) بهذه الطريقة، يتضمن تاريخ التغييرات المُجراة على الملف README.txt أيضًا تاريخ README.md. يمكننا أيضًا استخدام الأمر git mv لنقل ملف إلى مجلد جديد. أدخل الأوامر التالية لإنشاء مجلد جديد يدعى movetest وانقل الملف README.txt إليه: C:\Users\Al\wizcoin>mkdir movetest C:\Users\Al\wizcoin>git mv README.txt movetest/README.txt C:\Users\Al\wizcoin>git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) renamed: README.txt -> movetest/README.txt C:\Users\Al\wizcoin>git commit -m "Testing the moving of files in Git." [master 3ed22ed] Testing the moving of files in Git. 1 file changed, 0 insertions(+), 0 deletions(-) rename README.txt => movetest/README.txt (100%) يمكنك أيضًا إعادة تسمية ملف ونقله في الوقت نفسه عن طريق إدخال اسم وموقع جديدين في الأمر git mv. دعنا نعيد الملف README.txt إلى مكانه الأصلي في جذر مجلد المشروع ونمنحه اسمه الأصلي: C:\Users\Al\wizcoin>git mv movetest/README.txt README.md C:\Users\Al\wizcoin>git status On branch master Changes to be committed: (use "git reset HEAD <file>..." to unstage) renamed: movetest/README.txt -> README.md C:\Users\Al\wizcoin>git commit -m "Moving the README file back to its original place and name." [master 962a8ba] Moving the README file back to its original place and name. 1 file changed, 0 insertions(+), 0 deletions(-) rename movetest/README.txt => README.md (100%) لاحظ أنه على الرغم من عودة الملف README.md إلى مجلده الأصلي واسمه الأصلي، يتذكر مستودع غيت التنقلات وتغييرات الاسم. يمكنك رؤية هذا السجل باستخدام الأمر git log، الموضح في القسم التالي. عرض سجل الإيداع يُخرج الأمر git log قائمة بجميع الإيداعات: C:\Users\Al\wizcoin>git log commit 962a8baa29e452c74d40075d92b00897b02668fb (HEAD -> master) Author: Al Sweigart <al@inventwithpython.com> Date: Wed Sep 1 10:38:23 2021 -0700 Moving the README file back to its original place and name. commit 3ed22ed7ae26220bbd4c4f6bc52f4700dbb7c1f1 Author: Al Sweigart <al@inventwithpython.com> Date: Wed Sep 1 10:36:29 2021 -0700 Testing the moving of files in Git. --snip— يمكن لهذا الأمر عرض نص طويل، وإذا كان السجل أكبر من النافذة الطرفية، فيمكنك التنقل لأعلى أو لأسفل باستخدام مفتاحي الأسهم للأعلى والأسفل، وللخروج اضغط على الزر q. إذا كنت ترغب بإستعادة ملفاتك إلى إيداع أقدم من آخر إيداع، فأنت بحاجة أولًا إلى العثور على قيمة التعمية الخاصة بالإيداع commit hash، وهي سلسلة مكونة من 40 حرفًا من الأرقام بنظام العد الست عشري (تتكون من الأرقام والأحرف من A إلى F)، التي تعمل بمثابة معرّف فريد للإيداع. على سبيل المثال، قيمة التعمية الكاملة لأحدث إيداع في المستودع الخاص بنا هي: 962a8baa29e452c74d40075d92b00897b02668fb لكن من الشائع استخدام أول سبعة أرقام فقط: 962a8ba. بمرور الوقت، يمكن أن يصبح السجل طويلًا جدًا، ولذلك يجزِّء الأمر --oneline الخرج إلى قيمة تعمية مختصرة للإيداعات، إضافةً إلى السطر الأول من كل رسالة إيداع. أدخل git log --oneline في سطر الأوامر: C:\Users\Al\wizcoin>git log --oneline 962a8ba (HEAD -> master) Moving the README file back to its original place and name. 3ed22ed Testing the moving of files in Git. 15734e5 Deleting deleteme.txt from the repo to finish the deletion test. 441556a Adding a file to test Git deletion. 2a4c5b8 Added example code to README.md e1ae3a3 An initial add of the project files. إذا كان السجل لا يزال طويلًا جدًا، فيمكنك استخدام -n لتقييد الخرج إلى أحدث عملية إيداع. حاول إدخال git log --oneline -n 3 لعرض الإيداعات الثلاثة الأخيرة فقط: C:\Users\Al\wizcoin>git log --oneline -n 3 962a8ba (HEAD -> master) Moving the README file back to its original place and name. 3ed22ed Testing the moving of files in Git. 15734e5 Deleting deleteme.txt from the repo to finish the deletion test. يمكنك تنفيذ الأمر git show <hash>: <filename> لعرض محتويات الملف كما كانت في إيداع معين، إلا أن أدوات واجهة المستخدم الرسومية لغيت ستوفر واجهةً أكثر ملاءمة لفحص سجل المستودع مقارنةً بما توفره أداة سطر الأوامر لغيت. استعادة التغييرات القديمة لنفترض أنك تريد العمل مع إصدار سابق من الشيفرة المصدرية لأنك أدخلت خطأً، أو ربما حذفت ملفًا عن طريق الخطأ. يتيح لك نظام التحكم في الإصدار التراجع عن نسخة العمل الخاصة بك أو استعادتها rollback إلى محتوى إيداع سابق، ويعتمد الأمر الذي ستستخدمه على حالة الملفات في نسخة الشيفرة المصدرية التي تعمل عليها. ضع في الحسبان أن أنظمة التحكم في الإصدار تضيف المعلومات فقط، فحتى عند حذف ملف من المستودع، سيتذكر غيت الملف حتى تتمكن من استعادته لاحقًا. يؤدي التراجع عن تغيير في الواقع إلى إضافة تغيير جديد يعيد محتوى الملف إلى حالته في الإيداع السابق. ستجد معلومات مفصلة عن أنواع مختلفة من التراجع في المقال التراجع عن التعديلات في Git والمقال How to undo (almost) anything with Git من GitHub. التراجع عن التغييرات المحلية غير المودعة إذا أجريت تغييرات غير مودعة على ملف ولكنك تريد إعادته إلى الإصدار في آخر إيداع، فيمكنك تنفيذ الأمر git restore <filename>. في المثال التالي، نعدّل ملف README.md دون إدراج التعديل أو إيداعه: C:\Users\Al\wizcoin>git status On branch master Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: README.md no changes added to commit (use "git add" and/or "git commit -a") C:\Users\Al\wizcoin>git restore README.md C:\Users\Al\wizcoin>git status On branch master Your branch is up to date with 'origin/master'. nothing to commit, working tree clean يعود محتوى README.md إلى محتوى الإيداع الأخير بعد تنفيذ الأمر restore README.md، ويعد هذا التراجع فعالًا عن التغييرات التي أجريتها على الملف (ولكن لم تُدرج أو تُودع بعد). كن حذرًا، إذ لا يمكنك التراجع عن التراجع لاستعادة التغييرات. يمكنك أيضًا تنفيذ git checkout . للتراجع عن جميع التغييرات التي أجريتها على كل ملف في نسخة العمل الخاصة بك. التراجع عن إدراج ملف مدرج إذا كنت قد أدرجت ملفًا معدلًا عن طريق تنفيذ الأمر git add عليه، ولكنك بت الآن ترغب في إزالته من الإدراج حتى لا يتم تضمينه في الإيداع التالي، فعليك في هذه الحالة أن تنفّذ git restore --staged <filename> لإلغاء إدراجه: C:\Users\Al>git restore --staged README.md Unstaged changes after reset: M spam.txt يظل الملف README.md معدلًا كما كان قبل أن يُدرج الأمر git add الملف، إلا أن الملف لم يعد في الحالة المُدرجة. التراجع عن أحدث الإيداعات لنفترض إنشائك لعدة إيداعات غير مفيدة وتريد البدء من جديد من إيداع سابق. للتراجع عن عدد محدد من أحدث إيداعات، على سبيل المثال، ثلاثة، استخدم الأمر git revert -n HEAD ~ 3..HEAD. يمكنك استبدال 3 بأي عدد من الإيداعات. على سبيل المثال، لنفترض أنك تتبعت التغييرات على رواية غامضة كنت تكتبها ولديك سجل غيت التالي لجميع إيداعاتك ورسائل إيداعاتك. C:\Users\Al\novel>git log --oneline de24642 (HEAD -> master) Changed the setting to outer space. 2be4163 Added a whacky sidekick. 97c655e Renamed the detective to 'Snuggles'. 8aa5222 Added an exciting plot twist. 2590860 Finished chapter 1. 2dece36 Started my novel. قررت لاحقًا أنك تريد البدء من جديد في عملية التطوير بدءًا من الإيداع رقم 8aa5222، وهذا يعني أنه يجب عليك التراجع عن التغييرات من عمليات التنفيذ الثلاثة الأخيرة: de24642 و 2be4163 و 97c655e. نفّذ الأمر التالي للتراجع عن هذه التغييرات: git revert -n HEAD ~ 3..HEAD ثم نفذ الأمر git add . و git commit -m "<commit message>" لتطبيق هذا المحتوى، تمامًا كما تفعل مع أي تغيير آخر: C:\Users\Al\novel>git revert -n HEAD~3..HEAD C:\Users\Al\novel>git add . C:\Users\Al\novel>git commit -m "Starting over from the plot twist." [master faec20e] Starting over from the plot twist. 1 file changed, 34 deletions(-) C:\Users\Al\novel>git log --oneline faec20e (HEAD -> master) Starting over from the plot twist. de24642 Changed the setting to outer space. 2be4163 Added a whacky sidekick. 97c655e Renamed the detective to 'Snuggles'. 8aa5222 Added an exciting plot twist. 2590860 Finished chapter 1. 2dece36 Started my novel. تضيف مستودعات غيت عادةً المعلومات فقط، لذا فإن التراجع عن هذه الإيداعات لا يزال يتركها في سجل الإيداع. إذا أردت في أي وقت التراجع عن هذا "التراجع"، يمكنك التراجع عنه باستخدام git revert مرةً أخرى. التراجع إلى إيداع محدد لملف واحد نظرًا لأن الإيداعات تلتقط حالة المستودع كاملًا بدلًا من الملفات الفردية، فستحتاج إلى أمر مختلف إذا كنت تريد التراجع عن التغييرات لملف واحد. على سبيل المثال، لنفترض وجود مستودع غيت لمشروع برمجي صغير، وأنشأنا ملف eggs.py وأضفنا الدالة spam() و fish()، ثم أعدنا تسمية fish() إلى cheese(). سيبدو سجل المستودع كما يلي: C:\Users\Al\myproject>git log --oneline 895d220 (HEAD -> master) Adding email support to cheese(). df617da Renaming fish() to cheese(). ef1e4bb Refactoring fish(). ac27c9e Adding fish() function. 009b7c0 Adding better documentation to spam(). 0657588 Creating spam() function. d811971 Initial add. لكننا قررنا أنه نريد إعادة الملف إلى قبل إضافة fish() دون تغيير أي ملفات أخرى في المستودع. يمكننا هنا استخدام الأمر الآتي: git show <hash>: <filename> لعرض هذا الملف كما كان بعد إيداع معين. سيبدو الأمر على النحو التالي: C:\Users\Al\myproject>git show 009b7c0:eggs.py <contents of eggs.py as it was at the 009b7c0 commit> يمكننا ضبط محتويات eggs.py باستخدام git checkout <hash> - <filename> على هذا الإصدار وإيداع الملف الذي تغيّر كما هو معتاد. يغيّر الأمر git checkout نسخة العمل فقط. ما زلت بحاجة إلى تنظيم وإيداع هذه التغييرات مثل أي تغيير آخر: C:\Users\Al\myproject>git checkout 009b7c0 -- eggs.py C:\Users\Al\myproject>git add eggs.py C:\Users\Al\myproject>git commit -m "Rolled back eggs.py to 009b7c0" [master d41e595] Rolled back eggs.py to 009b7c0 1 file changed, 47 deletions(-) C:\Users\Al\myproject>git log --oneline d41e595 (HEAD -> master) Rolled back eggs.py to 009b7c0 895d220 Adding email support to cheese(). df617da Renaming bacon() to cheese(). ef1e4bb Refactoring bacon(). ac27c9e Adding bacon() function. 009b7c0 Adding better documentation to spam(). 0657588 Creating spam() function. d811971 Initial add. جرى التراجع عن ملف eggs.py، وبقي المستودع كما هو. إعادة كتابة تاريخ الإيداع إذا كنت قد أودعت ملفًا يحتوي على معلومات حساسة عن طريق الخطأ، مثل كلمات المرور أو مفاتيح واجهة برمجة التطبيقات أو أرقام بطاقات الائتمان، فلا يكفي تعديل هذه المعلومات وإجراء إيداع جديد. يمكن لأي شخص لديه حق الوصول إلى المستودع، سواء على حاسوبك أو عن بُعد، الرجوع إلى الإيداع الذي يتضمن هذه المعلومات. في الواقع، تعد إزالة هذه المعلومات من المستودع الخاص بك بحيث لا يمكن استردادها أمرًا صعبًا ولكنه ممكن، الخطوات الدقيقة لفعل ذلك هي خارج نطاق هذه السلسلة، ولكن يمكنك استخدام أمر git filter-Branch أو -الخيار الأفضل- أداة BFG Repo-Cleaner. يمكنك أن تقرأ عن كليهما على Removing sensitive data from a repository. أسهل إجراء وقائي لهذه المشكلة هو أن يكون لديك ملف secrets.txt أو secret.py أو ملف يحمل اسمًا مشابهًا تضع فيه معلومات حساسة وخاصة وتضيفها إلى ".gitignore" حتى لا تودعها أبدًا عن طريق الخطأ في المستودع. يمكن لبرنامجك قراءة هذا الملف والحصول على المعلومات الحساسة بدلًا من أن تكون موجودة مباشرةً في شيفرة المصدر الخاص به. منصة غيت هب GitHub وأمر git push على الرغم من أن مستودع غيت يمكن أن يوجد كاملًا على حاسوبك، إلا أن العديد من مواقع الويب المجانية يمكنها استضافة نسخ من المستودع عبر الإنترنت، مما يتيح للآخرين تحميل مشاريعك بسهولة والمساهمة فيها. غيت هب هو أكبر هذه المواقع، إذا احتفظت بنسخة من مشروعك عبر الإنترنت، فيمكن للآخرين إضافة الشيفرة إلى الشيفرة البرمجية الخاصة بك، حتى إذا كان الكمبيوتر الذي تعمل عليه مطفئًا. تعمل هذه النسخة أيضًا بمثابة نسخة احتياطية فعالة. ملاحظة: على الرغم من أن المصطلحات يمكن أن تسبب ارتباكًا، إلا أن غيت هو برنامج للتحكم في الإصدار يحتفظ بمستودع ويتضمن الأمر git، بينما غيت هب GitHub هو موقع ويب يستضيف مستودعات غيت عبر الإنترنت. انتقل إلى https://github.com وسجّل للحصول على حساب مجاني. انقر من صفحة غيت هب الرئيسية أو من نافذة المستودعات بصفحة ملفك الشخصي على زر جديد New لبدء مشروع جديد. أدخل wizcoin اسمًا للمستودع ووصف المشروع ذاته الذي قدمناه لأداة Cookiecutter سابقًا في "استخدام Cookiecutter لإنشاء مشاريع Python جديدة" في الصفحة 200، كما هو موضح في الشكل 6. حدّد المستودع على أنه عام Public وألغِ تحديد خانة إنشاء ملف اقرأني لهذا المستودع README Initialize this repository with a README باستخدام مربع الاختيار، لأننا سنستورد مستودعًا موجودًا. ثم انقر فوق إنشاء مستودع Create repository. تشبه هذه الخطوات فعليًا تنفيذ git init على موقع غيت هب. ستجد صفحة الويب الخاصة بمستودعاتك علىhttps://github.com/<username>/<repo_name> . يُستضاف في هذه الحالة مستودع wizcoin على https://github.com/asweigart/wizcoin. إضافة مستودع موجود إلى غيت هب لإضافة مستودع موجود من سطر الأوامر، أدخل ما يلي: C:\Users\Al\wizcoin>git remote add origin https://github.com/<github_username>/wizcoin.git C:\Users\Al\wizcoin>git push -u origin master Username for 'https://github.com': <github_username> Password for 'https://<github_username>@github.com': <github_password> Counting objects: 3, done. Writing objects: 100% (3/3), 213 bytes | 106.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0) To https://github.com/<your github>/wizcoin.git * [new branch] master -> master Branch 'master' set up to track remote branch 'master' from 'origin'. يضيف الأمر التالي غيت هب كأنه مستودع بعيد remote يتوافق مع المستودع المحلي الخاص بك: git remote add origin https://github.com/<github_username>/wizcoin.git ثم تُضيف أي إيداعات أجريتها في المستودع المحلي الخاص بك إلى المستودع البعيد باستخدام الأمر التالي: git push -u origin master يمكنك بعد هذه الإضافة الأولى إضافة جميع الإيداعات المستقبلية من المستودع المحلي الخاص بك ببساطة عن طريق تنفيذ git push. تُعد إضافة إيداعاتك إلى غيت هب بعد كل إيداع فكرة جيدة للتأكد من تحديث المستودع عن بُعد على غيت هب مع المستودع المحلي الخاص بك، ولكن الأمر ليس ضروريًا. من الممكن أيضًا فعل العكس: إنشاء مستودع جديد على غيت هب واستنساخه clone على حاسوبك. أنشئ مستودعًا جديدًا على موقع غيت هب، ولكن هذه المرة، حدّد خانة أنشئ ملف اقرأني لهذا المستودع README Initialize this repository with a README باستخدام مربع الاختيار. لاستنساخ هذا المستودع إلى حاسوبك، انتقل إلى صفحة المستودع على غيت هب وانقر على زر استنساخ أو تنزيل download لفتح نافذة يجب أن يبدو عنوان URL الخاص بها مثل https://github.com/<github_username>/wizcoin.git. استخدم عنوان URL لملف المستودع الخاص بك مع الأمر git clone لتنزيله على حاسوبك: C:\Users\Al>git clone https://github.com/<github_username>/wizcoin.git Cloning into 'wizcoin'... remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (3/3), done. remote: Total 5 (delta 0), reused 5 (delta 0), pack-reused 0 Unpacking objects: 100% (5/5), done. يمكنك الآن إيداع وادراج التغييرات باستخدام مستودع غيت تمامًا كما لو كنت قد نفذت git init لإنشاء المستودع. يُعد الأمر git clone مفيدًا أيضًا في حال دخول المستودع المحلي إلى حالة لا تعرف كيفية التراجع عنها. على الرغم من أنه ليس مثاليًا، يمكنك دائمًا حفظ نسخة من الملفات في مسار العمل الخاص بك، وحذف المستودع المحلي، واستخدام git clone لإعادة إنشاء المستودع. يحدث هذا السيناريو في كثير من الأحيان، حتى لمطوري البرامج ذوي الخبرة، وهو أساس النكتة الموجودة على xkcd.com. الخلاصة غيت هي أداة شاملة بها العديد من الميزات، وهذا الفصل يغطي فقط أساسيات نظام التحكم في الإصدارات. تتوفر لك العديد من الموارد لمعرفة المزيد حول ميزات غيت المتقدمة. لمزيد من المصادر حول أداة غبت Git، راجع قسم Git في أكاديمية حسوب ففيه عشرات المقالات المفيدة، وإن أردت التعمق أكثر، فنوصي بكتابين مجانيين يمكنك العثور عليهما عبر الإنترنت: Pro Git من تأليف سكوت شاركون Scott Charcon وكتاب Version Control by Example بواسطة اريك سينك Eric Sink . ترجمة -وبتصرف- لقسم من الفصل Organizing Your Code Projects With Git من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: حفظ التغييرات وعرضها باستخدام غيت Git في مشاريع بايثون فهم نظام التحكم بالإصدارات Git وأهمية استخدامه في مشاريع بايثون بدء العمل مع نظام إدارة الإصدارات جيت Git مدخل إلى نظام التحكم في النسخ Git
-
يمكنك الاستمرار بكتابة الشيفرة البرمجية لمشروعك بعد إضافة ملفات جديدة إلى المستودع، وعندما تريد حفظ تعديل (تعديلات) ما، يمكنك تنفيذ الأمر git add . لإدراج جميع الملفات المعدلة ثم كتابة الأمر git commit -m <commit message> لإيداع أو حفظ جميع الملفات المُدرجة، لكن فعل ذلك أسهل باستخدام الأمر git commit -am <commit message>: C:\Users\Al\wizcoin>git commit -am "Fixed the currency conversion bug." [master (root-commit) e1ae3a3] Fixed the currency conversion bug. 1 file changed, 12 insertions(+) إذا كنت تريد إيداع بعض الملفات المعدّلة فقط بدلًا من كل ملف معدل، فيمكنك حذف الخيار -a من -am وتحديد الملفات التي تريدها بعد رسالة التنفيذ، مثل: git commit -m <commit message> file1.py file2.py توفر رسالة الإيداع تلميحًا للاستخدام المستقبلي: إنها عبارة عن رسالة تذكرك بالتغييرات التي أجريتها في هذا الإيداع. قد يكون من المغري كتابة رسالة قصيرة عامة، مثل "رمز مُحدَّث" أو "إصلاح بعض الأخطاء" أو حتى "x" فقط لأن الرسائل الفارغة غير مسموح بها، ولكن إذا احتجت إلى التراجع عن إصدار الكود البرمجي بعد ثلاثة أسابيع من القيام بهذا الأمر ستجد أن كتابة رسائل الإيداع الواضحة ستوفر عليك الكثير من الجهد وتساعدك كي تحدد بدقة وقت الإصدار البرمجي السابق من الشيفرة البرمجية الخاصة بك الذي تحتاج للعودة إليه. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن إذا نسيت إضافة راية سطر الأوامر -m "<message>"، فسوف يفتح لك غيت محرر نص فيم Vim في الطرفية. لكن شرح التعامل مع محرر فيم خارج نطاق هذه السلسلة، لذا اضغط على مفتاح ESC وأدخل !qa للخروج بأمان من محرر فيم Vim وإلغاء عملية الإيداع، ثم أدخل الأمر git commit مرةً أخرى لكن وهذه المرة باستخدام سطر الأوامر -m "<message>". للحصول على أمثلة حول شكل رسائل الإيداع الاحترافية، تحقق من سجل الإيداع الخاص بإطار عمل ويب جانغو Django على الرابط، فجانغو مشروع كبير ومفتوح المصدر، لذا تكتب الإيداعات المتكررة بصيغة رسائل رسمية. قد تفي رسائل الإيداع المختصرة والغامضة في حال كان الإيداع غير متكرر كما في حال المشاريع البرمجية الشخصية الصغيرة الخاصة بك، لكن في مشروع ضخم مثل جانغو أكثر من 1000 مساهم يعدلون على أكوادها البرمجية، لذا ستتسبب رسائل الإيداع غير الواضحة في تشتت فريق المساهمين وعدم فهمهم للتعديلات التي تم إيداعها. الملفات مودعة الآن بأمان في مستودع غيت. نفّذ git status مرةً أخرى لعرض حالتها: C:\Users\Al\wizcoin>git status On branch master nothing to commit, working tree clean من خلال إيداع الملفات المُدرجة، تُنقل إلى حالة الإيداع، فقد أخبرنا غيت هنا أن شجرة العمل working tree نظيفة؛ بمعنى آخر، لا توجد ملفات معدلة أو مُدرجة. باختصار، عندما أضفنا الملفات إلى مستودع غيت، انتقلت الملفات من حالة غير مُتتبعة (untracked) إلى مُدرجة (staged) ثم إلى حالة الإيداع (committed). الملفات جاهزة الآن للتعديل في المستقبل. لاحظ أنه لا يمكنك تثبيت المجلدات في مستودعات غيت، إذ يُضمّن غيت المجلدات تلقائيًا في المستودع عند تنفيذ ملف فيها، ولكن لا يمكنك إنشاء مجلد فارغ. إذا كان هناك خطأ إملائي في رسالة الإيداع، فيمكنك إعادة كتابتها باستخدام الأمر التالي: git commit --amend -m "<new commit message>" استخدام git diff لعرض التغييرات قبل الإيداع يجب عليك مراجعة التغييرات التي ستودعها عند تنفيذ git commit قبل إيداع الشيفرة البرمجية، إذ يمكنك عرض الاختلافات بين الشيفرة الموجودة حاليًا في مجلد العمل الخاص بك والشيفرة في آخر إيداع باستخدام الأمر git diff. دعنا ننظر إلى مثال عن استخدام git diff؛ افتح الملف "README.md" في محرر النصوص أو بيئة التطوير IDE، إذ ينبغي أن تكون قد أنشأت هذا الملف عند تنفيذ أداة Cookiecutter. إذا لم يكن موجودًا، فأنشئ ملفًا نصيًا فارغًا واحفظه باسم README.md. هذا الملف هو بتنسيق مارك داون Markdown، ولكن كما هو الحال مع نصوص بايثون، فهو مكتوب على أنه نص عادي. غيّر TODO - fill this in later في قسم Quickstart Guide بما يلي واحتفظ بالخطأ الإملائي في xample في الوقت الحالي، وسنصححه لاحقًا: Quickstart Guide ---------------- Here's some xample code demonstrating how this module is used: >>> import wizcoin >>> coin = wizcoin.WizCoin(2, 5, 10) >>> str(coin) '2g, 5s, 10k' >>> coin.value() 1141 قبل أن نضيف README.md ونودعه، نفّذ الأمر git diff لرؤية التغييرات التي أجريناها: C:\Users\Al\wizcoin>git diff diff --git a/README.md b/README.md index 76b5814..3be49c3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,14 @@ To install with pip, run: Quickstart Guide ---------------- -TODO - fill this in later +Here's some xample code demonstrating how this module is used: + + >>> import wizcoin + >>> coin = wizcoin.WizCoin(2, 5, 10) + >>> str(coin) + '2g, 5s, 10k' + >>> coin.value() + 1141 Contribute ---------- يوضّح الخرج أن ملف README.md في نسخة العمل الخاصة بك قد تغير عن README.md كما هو موجود في آخر إيداع من المستودع. أزيلت الأسطر التي تبدأ بعلامة الطرح -، واُضيفت الأسطر التي تبدأ بعلامة الجمع +. أثناء مراجعة التغييرات، ستلاحظ أيضًا أننا ارتكبنا خطأ إملائيًا من خلال كتابة xapmle بدلًا من example. يجب ألا نودع هذا الخطأ المطبعي. لذا دعنا نصححه ثم ننفذ git diff مرةً أخرى لفحص التغيير وإضافته وإيداعه في المستودع: C:\Users\Al\wizcoin>git diff diff --git a/README.md b/README.md index 76b5814..3be49c3 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,14 @@ To install with pip, run: Quickstart Guide ---------------- -TODO - fill this in later +Here's some example code demonstrating how this module is used: --snip-- C:\Users\Al\wizcoin>git add README.md C:\Users\Al\wizcoin>git commit -m "Added example code to README.md" [master 2a4c5b8] Added example code to README.md 1 file changed, 8 insertions(+), 1 deletion(-) التصحيح مودع الآن بأمان في المستودع. استخدام git difftool لعرض التغييرات ضمن واجهة رسومية GUI من الأسهل رؤية التغييرات باستخدام برنامج يستخدم واجهة رسومية، إذ يمكنك تنزيل WinMerge على نظام ويندوز؛ وهو برنامج مفتوح المصدر ومجاني لاستعراض الفرق بين الملفات ، ثم تثبيته. أما في نظام لينكس أوبنتو، يمكنك تثبيت إما Meld باستخدام الأمر التالي: sudo apt-get install meld أو تثبيت Kompare باستخدام الأمر التالي: sudo apt-get install kompare بينما يمكنك على ماك macOS تثبيت tkdiff باستخدام الأوامر التي تثبّت وتُهيّئ Homebrew ثم استخدام Homebrew لتثبيت tkdiff: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)" brew install tkdiff يمكنك ضبط غيت لاستخدام هذه الأدوات عن طريق تنفيذ الأمر: git config diff.tool <tool_name> إذ يكون <tool_name> هو winmerge أو tkdiff أو meld أو kompare، ثم نفذ الأمر git difftool <filename> لعرض التغييرات المُجراة على ملف في واجهة المستخدم الرسومية، كما هو موضح في الشكل 5. الشكل 5: أداة واجهة المستخدم الرسومية -في هذه الحالة WinMerge- وهي أسهل للقراءة من إخراج نص git diff. إضافةً إلى ذلك، يمكنك تنفيذ الأمر التالي: git config --global difftool.prompt false بهذا لا يطلب غيت التأكيد في كل مرة تريد فيها فتح أداة diff. وإذا ثبتت واجهة المستخدم الرسومية لغيت، فيمكنك أيضًا ضبطه بحيث يستخدم هذه الأدوات، أو قد يأتي مع أداة مقارنة مرئية خاصة به. كم مرة يجب أن أحفظ التغييرات؟ على الرغم من أن نظام التحكم في الإصدارات يسمح لك باستعادة ملفاتك إلى إيداع سابق، فقد تتساءل عن عدد المرات التي يجب أن تجري فيها الإيداع (الحفظ)؛ فإذا كنت تودع بصورةٍ متكررة، فستواجه مشكلة في فرز عدد كبير من الإيداعات غير المهمة للعثور على إصدار الشيفرة الذي تبحث عنه؛ أما إذا كنت تودع بصورةٍ غير متكررة، فسيحتوي كل إيداع على عدد كبير من التغييرات، وستؤدي العودة إلى إيداع معين إلى التراجع عن تغييرات أكثر مما تريد. يميل المبرمجون عمومًا إلى الإيداع بصورةٍ أقل مما ينبغي. يجب عليك إيداع شيفرة عند إكمال جزء كامل من الوظائف، مثل ميزة، أو صنف، أو إصلاح خطأ. لا تودع أي شيفرة تحتوي على أخطاء الصياغة أو أنها معطلة بصورة واضحة. يمكن أن تتكون الإيداعات من بضعة أسطر من الشيفرة البرمجية التي جرى تغييرها أو عدة مئات، ولكن في كلتا الحالتين، يجب أن تكون قادرًا على العودة إلى أي إيداع سابق ولا يزال لديك برنامج يعمل. يجب عليك دائمًا إجراء أي اختبارات وحدة قبل الإيداع. من الناحية المثالية، يجب أن يجتاز الإيداع جميع اختباراتك، وإذا لم ينجح، فاذكر ذلك في رسالة الإيداع. الخلاصة يتتبع غيت git الملفات في مجلد العمل الخاص بك التي يمكن أن توجد جميعها في واحدة من ثلاث حالات: مُودعة committed (تسمى أيضًا غير معدلة أو نظيفة)، أو معدلة modified، أو مُدرجة staged. تحتوي أداة سطر أوامر غيت على العديد من الأوامر، مثل git status أو git log التي تتيح لك عرض هذه المعلومات، ولكن يمكنك أيضًا تثبيت العديد من أدوات خارجية إضافية لواجهة المستخدم الرسومية لغيت. ينشئ الأمر git init مستودعًا جديدًا فارغًا على حاسوبك. ينسخ الأمر git clone المستودع من خادم بعيد، مثل موقع غيت هب GitHub الشهير. في كلتا الحالتين. بمجرد حصولك على المستودع، يمكنك استخدام git add و git commit لإجراء تغييرات في مستودعك، واستخدام git push لدفع هذه الإيداعات إلى مستودع غيت =هب عن بُعد. وصفنا أيضًا العديد من الأوامر في هذا الفصل للتراجع عن الإيداعات المُجراة حيث يسمح لك إجراء التراجع بالعودة إلى إصدار سابق من ملفاتك. ترجمة -وبتصرف- للفصل Organizing Your Code Projects With Git من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: فهم نظام التحكم بالإصدارات Git وأهمية استخدامه في مشاريع بايثون python مبادئ Git الأساسية بدء العمل مع نظام إدارة الإصدارات جيت Git الأسئلة العشرة الأكثر تكرارًا حول Git
-
إن التعليقات comments والتوثيق documentation في الشيفرة المصدرية هي بأهمية الشيفرة البرمجية ذاتها، سبب ذلك هو أن عملية تطوير البرمجيات لا تنتهي أبدًا وتحتاج دائمًا للقيام بتغييرات، وذلك إما لإضافة ميّزات جديدة أو لإصلاح المشاكل، ولكن لا تستطيع تغيير الشيفرة إذا لم تفهمها لذا يجب ابقاءها بحالة صالحة للقراءة، كما كتب علماء الحاسوب هارولد ابلسون Harold Abelson وجيرالد جاي Gerald Jay وجولي سوسمان Julie Sussman: تسمح لك التعليقات -بالإضافة إلى سلاسل التوثيق النصية docstrings وتلميح الأنواع type hints التي سنناقشها لاحقًا- بالمحافظة على وضوح الشيفرة، إذ أن التعليقات هي شروحات بلغة بشرية قصيرة وبسيطة تُكتب مباشرة في الشيفرة المصدرية ويتجاهلها الحاسوب، تقدم التعليقات ملاحظات مساعدة وتحذيرات ورسائل تذكير للآخرين الذين لم يكتبوا الشيفرة، أو أحيانًا لمبرمجين الشيفرة في المستقبل. سأل معظم المبرمجون أنفسهم في وقتٍ ما "من كتب هذه الفوضى الغير مقروءة؟" فقط ليأتي الجواب ضمن التعليقات "أنا!". تركز هذه المقالة على التعليقات لتضمين التوثيق داخل الشيفرة الخاصة بك لجعلها أكثر ملائمة للقراءة، إن التوثيقات الخارجية مثل دليل المستخدم والدروس التعليمية على الشبكة والمراجع مهمة أيضًا إلا أنها خارج نطاق موضوعنا، إذا أردت أن تعرف أكثر بخصوص التوثيقات الخارجية راجع مولد توثيق سفينكس Sphinx. ستتحدث المقالة أيضًا عن سلاسل التوثيق النصية docstrings هي نوع توثيق خاص بلغة بايثون للدوال functions والتوابع methods وأيضًا الوحدات modules الخاصة بك. عندما تحدد تعليقات بصيغة سلاسل التوثيق النصية، ستسهّل الأدوات الآلية automated tools، مثل مولدات التوثيق، أو وحدة help() المبنية مسبقًا في بايثون على المطورين إيجاد المعلومات عن الشيفرة الخاصة بك. التعليقات Comments تدعم بايثون تعليقات السطر الواحد والأسطر المتعددة مثل كل لغات البرمجة، أي نص يأتي بعد إشارة # ونهاية السطر هو تعليق سطر واحد، وعلى الرغم من أنه ليس لبايثون صياغة مخصصة لتعليقات الأسطر المتعددة يمكن استخدام السلاسل النصية المتعددة الأسطر بثلاث مزدوجات بدلًا عن ذلك، حيث لا تتسبب قيمة السلسلة النصية هذه بأن يقوم مفسر بايثون بأي شيء، انظر إلى هذا المثال: # هذا تعليق سطري """هذه سلسلة نصية متعددة الأسطر وتعمل كتعليق متعدد الأسطر أيضًا """ إذا استمر التعليق الخاص بك لأكثر من سطر فمن الأفضل استخدام تعليق واحد متعدد الأسطر بدلًا من عدة تعليقات سطر واحد متتالية التي ستكون قراءتها أصعب كما نرى هنا: """هذه طريقة جيدة لكتابة تعليق يمتد لعدة أسطر """ # هذه ليست طريقة جيدة # لكتابة تعليق # يمتد لعدة أسطر. تعدّ التعليقات والتوثيقات فكرة ثانوية في عملية البرمجة ويعتقد البعض بأنها تتسبب بالضرر أكثر من الإفادة، التعليقات ليست اختيارية إذا أردت كتابة شيفرة احترافية ومقروءة، سنكتب في هذا القسم تعليقات مفيدة تزيد معرفة القارئ دون أن تؤثر على قابلية قراءة البرنامج. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تنسيق التعليق لننظر إلى بعض التعليقات التي تتّبع ممارسات تنسيق جيّدة: 1 # هنا تعليق بخصوص الشيفرة البرمجية التالية: someCode() 2 # هنا كتلة تعليق أطول تمتد إلى عدة أسطر # باستخدام عدّة تعليقات سطر واحد على التوالي 3 # # تُعرف هذه الكتل بكتل التعليقات if someCondition: 4 # هنا تعليق بخصوص الشيفرة البرمجية التالية 5 someOtherCode() # هنا تعليق سطري يجب أن تكون التعليقات في سطرها الخاص بدلًا من نهاية سطر الشيفرة، يجب أن تكون التعليقات في معظم الوقت جمل كاملة سليمة الكتابة مع علامات الترقيم بدلًا من عبارات أو كلمات وحيدة (تعليق 1). الاستثناء هو أن التعليقات يجب أن تتبع لحدود طول السطر الواحد الخاص بالشيفرة المصدرية ذاته، ويمكن للتعليقات التي تمتد لأسطر متعددة أن تستخدم تعليقات سطر واحد متعددة على التوالي وذلك يُعرف بكتلة التعليقات block comments (تعليق 2). يمكننا فصل الفقرات في كتل تعليقات باستخدام فراغ وتعليق سطر واحد (تعليق 3). يجب للتعليقات أن تكون بمستوى مماثل للمسافة البادئة للشيفرة التي تُعلق عليها (تعليق 4). تُدعى التعليقات التي تتبع سطر الشيفرة بالتعليقات السطرية inline comments (تعليق 5) ويجب على الأقل ترك فراغين بين الشيفرة والتعليق. يجب أن تحتوي تعليقات السطر الأحادي على فراغ واحد بعد إشارة #: #لا تكتب التعليقات مباشرةً بعد إشارة التعليق يمكن أن تحتوي التعليقات على رابط URL مع المعلومات المتعلقة به، إلا أنه لا يجب استبدال التعليقات بالروابط لأن المحتوى المتعلق بالرابط يمكن أن يختفي من الشبكة في أي وقت: # إليك شرحًا مفصلًا عن بعض الجوانب في هذه الشيفرة البرمجية # والموجودة على الرابط التالي، لمزيد من المعلومات اذهب إلى https://example.com تصف الاصطلاحات المذكورة سابقًا النمط أكثر من وصفها للمحتوى، إلا أنها تُسهِّل من قراءة التعليق، كلما كان التعليق أسهل للقراءة، زاد اهتمام المبرمجين فيها، وتكون التعليقات مفيدة فقط عندما يستطيع المبرمجون قراءتها. التعليقات السطرية تأتي التعليقات السطرية inline comments في نهاية سطر الشيفرة كما في الحالة التالية: while True: # استمرّ بسؤال اللاعب لحين إدخاله لحركة صالحة تكون التعليقات السطرية مختصرة، لذا يمكن أن تتسع ضمن حدود طول السطر الموضوعة في دليل تنسيق البرنامج، هذا يعني أنها في أغلب الأحوال تكون قصيرة لحد عدم إعطاء معلومات كافية. إذا كنت تريد استخدام التعليقات السطرية، فانتبه لجعل التعليق يشرح السطر الذي يسبقه فقط، يجب وضع التعليق السطري في سطره الخاص إذا كان يحتاج لمساحة أكثر أو كان يشرح أكثر من سطر من الشيفرة. أحد الاستخدامات الشائعة المناسبة للتعليقات السطرية هو شرح هدف متغير ما أو إعطاء بعض من السياق بخصوصه، وتُكتب هذه التعليقات السطرية على تعبير الإسناد الذي يُنشئ المتغير: TOTAL_DISKS = 5 # المزيد من الأقراص سيزيد صعوبة الأحجية استخدام ثاني شائع للتعليقات السطرية هو إضافة سياق لقيم المتغيرات عندما تريد انشائها: month = 2 # تتراوح قيمة الأشهر من 0 (أي يناير) إلى 11 (أي ديسمبر) catWeight = 4.9 # الوزن بوحدة الكيلوجرام website = 'inventwithpython.com' # لا تُضمّن "https://" في بداية القيمة لا يجب على التعليقات السطرية أن تحدد نوع بيانات المتغير، لأن هذا واضح من تعبير الإسناد إلا أذا كان ذلك يشكل تعليقًا لتلميح النوع type hints وهو ما سنتحدث عنه لاحقًا. التعليقات التوضيحية يجب أن تشرح التعليقات بالعادة لماذا تكون الشيفرة مكتوبة بهذا الشكل بدلًا من طريقة عمل الشيفرة أو كيفية عملها، حتى مع تنسيق الشيفرة المناسب واصطلاحات التسميات المفيدة التي تحدثنا عنها سابقًا فلا يمكن للشيفرة شرح نوايا المبرمج، فقد تنسى تفاصيل الشيفرة التي كتبتها بعد أسابيع، بالتالي يجب عليك في الوقت الحاضر كتابة تعليقات شيفرة توضيحية لمنع نفسك المستقبلي من لعن نفسك الماضي. لدينا هنا مثال عن تعليق غير مفيد يشرح ماذا تفعل الشيفرة بدلًا من تلميح سبب كتابة الشيفرة، إذ يوضح هذا التعليق أمرًا مفهومًا: >>> currentWeekWages *= 1.5 # ضرب أجور الأسبوع الحالي بمقدار 1.5 إن هذا التعليق أكثر من غير مفيد، فمن الواضح من الشيفرة أن المتغير currentWeekWages مضروب بـالقيمة 1.5 لذا إهمال هذا التعليق بالكامل من شأنه تبسيط الشيفرة الخاصة بك. إن التعليق التالي سيكون أفضل بكثير: >>> currentWeekWages *= 1.5 # أخذ نسبة الأجرة ونصف بالحسبان يشرح هذا التعليق النيّة من هذه الشيفرة بدلًا من تكرار كيفية عمل الشيفرة، إذ يعطي سياقًا لا تستطيع حتى الشيفرة المكتوبة بشكل جيد تقديمها. تعليقات الخلاصة شرح نيّة المبرمج ليست الطريقة الوحيدة التي تكون فيها التعليقات مفيدة، تسمح التعليقات المختصرة التي تلخص عدة أسطر من الشيفرة للقارئ أن ينظر إلى الشيفرة بشكل سريع ويحصل على فكرة عامة عما تفعله. يستخدم المبرمجون عادة فراغ فارغ لفصل "فقرات" الشيفرة عن بعضها وتشغل تعليقات التلخيص summary comments سطرًا واحد في بداية هذه الفقرات، وعلى عكس التعليقات السطرية التي تشرح سطرًا واحدًا من الشيفرة، تصف تعليقات التلخيص ما تفعله الشيفرة على درجة أعلى من التجريد. يمكن في هذا المثال أن نعرف من قراءة السطور الأربعة من الشيفرة أنها تضبط متغير playerTurn لقيمة تمثل اللاعب الضد، ولكن تعليق السطر الواحد القصير يريح القارئ من قراءة الشيفرة ومعرفة هدف القيام بذلك: # تبديل الدور إلى اللاعب الآخر if playerTurn == PLAYER_X: playerTurn = PLAYER_O elif playerTurn == PLAYER_O: playerTurn = PLAYER_X توزيع تعليقات خلاصة بالشكل هذا في البرنامج الخاص بك يُسهل علينا النظر إليه بشكل سريع، يمكن للمبرمج أن يتفحص الشيفرة بشكل دقيق بخصوص أي نقطة تهمه، وتمنع تعليقات الخلاصة المبرمجين من أخذ أفكار خاطئة عما تفعله الشيفرة، إذ يمكن لتعليق تلخيص قصير أن يؤكد أن المطور فهم بشكل مناسب كيفية عمل الشيفرة. تعليقات "الدروس المستفادة" طُلب مني عندما كنت أعمل في شركة برمجيات أن أُكيّف مكتبة رسوم بيانية لكي تتعامل مع تحديثات في الوقت الفعلي لملايين نقاط البيانات في المخطط. المكتبة التي كنا نستخدمها تستطيع إما تحديث المخطط في الوقت الفعلي أو تدعم مخططات لملايين نقاط البيانات ولكن ليس الأمرين معًا؛ توقعت أن أُنهي المهمة خلال بضعة أيام وفي الأسبوع الثالث مازلت اعتقد انني أستطيع ان انتهي خلال بضعة أيام، كل يوم كان الحل قريبًا جدًا وفي خلال الأسبوع الخامس كان لدي نموذج أولي يعمل. خلال كل تلك الفترة تعلمت الكثير من التفاصيل عن كيفية عمل مكتبة الرسوم البيانية وما هي قدراتها وحدودها، ثم قضيت بضع ساعات اكتب هذه التفاصيل في تعليق بطول صفحة ووضعته في الشيفرة المصدرية، عرفت أن أي شخص يحتاج للتعديل على الشيفرة الخاصة بي لاحقًا سيواجه هذه المشاكل البسيطة نفسها التي واجهتها، وهذا التوثيق الذي كتبته سيوفر عليهم أسابيع من الجهد. ربما يمتد تعليق "الدروس المستفادة" -كما اسميهم- لعدة فقرات مما يجعله يبدوا أنّه لا ينتمي إلى الشيفرة المصدرية ولكن المعلومات المحتواة فيه هي كنز لمن يحتاج المحافظة على هذه الشيفرة، لا تخف من كتابة تعليقات طويلة ومفصلة في الشيفرة المصدرية الخاصة بك لشرح كيفية عمل شيء ما، بالنسبة للمبرمجين الآخرين العديد من هذه التفاصيل ستكون غير معروفة أو غير مفهومة أو مهملة. يمكن لمطوري البرمجيات الذين لا يحتاجون لهذه التفاصيل بأن يتجاهلوها ببساطة، إلا أن المطورين الذين يحتاجونهم سيكونون ممتنين لوجودهم. تذكّر أن تعليق الدروس المستفادة هو ليس مثل توثيقات الوحدات أو الدوال (التي تتعامل معها سلاسل التوثيق النصية)، وهو ليس درس تعليمي لمستخدمي البرنامج، بل أن تعليقات الدروس المستفادة هي للمطورين الذين يقرؤون الشيفرة المصدرية. لأن تعليق الدروس المستفادة الخاص بي يتعلق بمكتبة رسوم بيانية ذات مصدر مفتوح ويمكن أن يفيد الآخرين فقد نشرته في موقع السؤال والجواب العام Stackoverflow.org بحيث يتسنّى للآخرين الذين هم في نفس حالتي إيجاده. التعليقات القانونية لدى بعض الشركات البرمجية أو المشاريع المفتوحة المصدر سياسات تضم حقوق النشر وترخيص البرمجيات ومعلومات عن المؤلف في التعليقات في أعلى كل ملف شيفرة مصدرية لأسباب قانونية، يجب أن تتألف هذه التوصيفات من عدة أسطر على الأكثر وتشبه التالي: """Cat Herder 3.0 Copyright (C) 2021 Al Sweigart. All rights reserved. See license.txt for the full text.""" راجع إذا أمكن ملفات خارجية أو مواقع تحوي النص الكامل للرخصة بدلًا من ضم كل الرخصة فوق كل ملف شيفرة مصدرية، إنه من المتعب المرور عبر عدة صفحات من النصوص كلما تفتح ملف شيفرة مصدرية، وضم الرخصة كاملةً لا يضيف أي حماية قانونية إضافية. تعليقات مهنية أخبرني زميل أكبر مني أحترمه كثيرًا في عملي البرمجي الأول أننا إذا أرفقنا الشيفرة المصدرية لمنتجنا للعملاء أحيانًا فإنه من المهم أن تكون للتعليقات نبرة مهنية. يبدو وقتها أنني كتبت “يا إلهي ما هذا!” في أحد التعليقات لجزء مثير للإحباط من الشيفرة، شعرت عندئذٍ بالحرج واعتذرت مباشرةً وعدلت التعليق، ومنذ تلك اللحظة حافظت على مستوى المهنيّة الخاص بشيفرتي البرمجية حتى في المشاريع الشخصية. ربما يُغريك كتابة مزحة أو التنفيس عن غضبك في تعليقات برنامجك، ولكن اعتد تجنّب القيام بذلك، فأنت لا تعرف من سيقرأ الشيفرة الخاص بك في المستقبل ومن السهل ألّا تُفهم نبرة النص، كما وضحنا سابقًا، السياسة الفُضلى هي كتابة تعليقاتك بطريقة مهذبة ومباشرة بدون مزاح. تعليقات وسوم الشيفرة البرمجية والأشياء التي يجب القيام بها يترك المبرمجين أحيانًا تعليقات قصيرة لتذكيرهم بخصوص المهام التي يجب القيام بها، ويأخذ ذلك شكل وسوم الشيفرة البرمجية codetags وهو تعليق بحروف كبيرة مثل TODO متبوع بشرح قصير، يجب استخدام أدوات إدارة المشروع في الحالة المثالية لمتابعة هذه المشاكل بدلًا من دفنها في الشيفرة المصدرية الخاصة بك، ولكن يمكن استخدام هذه التعليقات للمشاريع الشخصية الصغيرة التي لا تستخدم العديد من الأدوات، إذ يمكن لتعليق TODO أن يخدم كتذكير مفيد، كمثال على ذلك: _chargeIonFluxStream() # TODO: النظر إلى سبب فشل هذه الدالة كل يوم ثلاثاء بإمكانك استخدام عدد من وسوم الشيفرة لهذه التنبيهات كما يلي: TODO يمثّل تذكيرًا عام عن عمل يجب القيام به FIXME يمثّل تذكيرًا بخصوص عدم عمل جزء من الشيفرة بشكل كامل HACK يمثّل تذكيرًا بأن هذا الجزء من الشيفرة يعمل ولكن بصعوبة ويجب تطوير هذه الشيفرة XXX يمثّل تنبيهًا عامًا وعادةً ما يكون ذو ثقل كبير يجب إلحاق هذه التسميات ذات الحروف الكبيرة بشرح مفصل عن المهمة أو المشكلة، وبعدها يمكن البحث في الشيفرة المصدرية عن هذه التسميات لإيجاد الشيفرة التي بحاجة إصلاح، السيئة من هذه التعليقات أنه يمكن نسيان هذه التذكرات إن لم تكن تقرأ هذا القسم من الشيفرة الخاصة بك الموجودة فيها تلك الوسوم. لا يجب أن تستبدل وسوم الشيفرة أدوات متابعة المشاكل أو تقارير الأخطاء الرسمية. إذا أردت استخدام وسوم الشيفرة في الشيفرة الخاصة بك أنصح بإبقائها بسيطة باستخدام TODO وتجاهل الباقي. التعليقات السحرية وترميز الملف المصدري لربما لمحت ملف مصدري .py مع شيء يشبه هذه الأسطر في أعلى الملف: 1 #!/usr/bin/env python3 2 # -*- coding: utf-8 -*- تقدم هذه التعليقات السحرية magic comments التي تظهر دائمًا في أعلى الملف معلومات بخصوص المفسر أو الترميز، إذ يخبر سطر شيبانج shebang الأول المبدوء بإشارتي !# نظام التشغيل بالمفسر الذي يجب أن يُستخدم لتنفيذ التعليمات في الملف. التعليق السحري الثاني هو تعريف للترميز، ويُعرّف في هذه الحال الترميز UTF-8 كنمط ترميز يونيكود لاستخدامه في الملف المصدري. لا تحتاج على الإطلاق عمومًا لضم هذا السطر لأن معظم المحرّرات وبيئات التطوير المتكاملة IDEs تحفظ ملفات الشيفرة المصدرية باستخدام الترميز UTF-8، وتعامل إصدارات بايثون بدءًا من 3.0 الترميز UTF-8 كترميز الافتراضي. يمكن أن تحتوي الملفات المرمزة باستخدام UTF-8 على أي مَحرف لذا يبقى ملف المصدر .py صالحًا حتى لو كان يحتوي على حروف انكليزية أو صينية أو عربية. لمقدمة عن اليونيكود وترميز السلاسل النصية أرشح منشور مدوّنة نيد باتشيلدر Ned Batchelder "اليونيكود العملي". سلاسل التوثيق النصية Docstrings سلاسل التوثيق النصية docstrings هي تعليقات بأسطر متعددة تظهر في أعلى ملف الوحدة المصدري ".py"، أو مباشرةً بعد تعليمة class، أو def، وتقدم توثيقات عن الوحدة module، أو الصنف class أو الدالة funtion) أو التوابع المعرّفة. تُستخدم أدوات توليد التوثيقات الآلية سلاسل التوثيق النصية هذه لإنشاء ملفات توثيق خارجية، مثل ملفات المساعدة، أو صفحات الويب. يجب أن تستخدم سلاسل التوثيق النصية تعليقات متعددة الأسطر مع ثلاث علامات تنصيص مزودجة بدلًا من التعليقات أحادية السطر التي تبدأ بإشارة المربع #، كما ينبغي أن تستخدم سلاسل التوثيق النصية ثلاث علامات تنصيص مزودجة من أجل سلاسل ثلاثية الاقتباس بدلًا من ثلاث علامات تنصيص أحادية. إليك مثالًا عن ملف sessions.py في الوحدة الشهيرة requests: 1 # -*- coding: utf-8 -*- 2 """ requests.session ~~~~~~~~~~~~~~~~ This module provides a Session object to manage and persist settings across requests (cookies, auth, proxies). """ import os import sys --snip— class Session(SessionRedirectMixin): 3 """A Requests session. Provides cookie persistence, connection-pooling, and configuration. Basic Usage:: >>> import requests >>> s = requests.Session() >>> s.get('https://httpbin.org/get') <Response [200]> --snip-- def get(self, url, **kwargs): 4 r"""Sends a GET request. Returns :class:`Response` object. :param url: URL for the new :class:`Request` object. :param \*\*kwargs: Optional arguments that ``request`` takes. :rtype: requests.Response """ --snip-- يحتوي request الخاص بملف sessions.py على سلاسل توثيق نصية من أجل الوحدة (تعليق 2) والصنف Session (تعليق 3) وتابع get() الخاص بصنف Session (تعليق 4). لاحظ أنه على الرغم من أن سلسلة التوثيق النصية للوحدة يجب أن تكون أول سلسلة نصية تظهر في الوحدة، إلا أنها يجب أن تأتي بعد أي تعليق سحري، مثل سطر شيبانج shebang المبدوء بإشارة # أو تعريف الترميز (تعليق 1). يمكن استرجاع سلاسل التوثيق النصية لاحقًا من أجل وحدة أو صنف أو تابع عن طريق التحقق من سمة الكائن __doc__ الخاصة به، إذ يمكننا هنا مثلًا فحص سلسلة التوثيق النصية لمعرفة المزيد عن وحدة sessions وصنف Session والتابع get(): >>> from requests import sessions >>> sessions.__doc__ '\nrequests.session\n~~~~~~~~~~~~~~~~\n\nThis module provides a Session object to manage and persist settings across\nrequests (cookies, auth, proxies).\n' >>> sessions.Session.__doc__ "A Requests session.\n\n Provides cookie persistence, connection-pooling, and configuration.\n\n Basic Usage::\n\n >>> import requests\n --snip-- >>> sessions.Session.get.__doc__ 'Sends a GET request. Returns :class:`Response` object.\n\n :param url: URL for the new :class:`Request` object.\n :param \\*\\*kwargs: --snip-- يمكن أن تستخدم أدوات التوثيقات الآلية سلاسل التوثيق النصية لتأمين معلومات مناسبة للسياق، ومن هذه الأدوات هي دالة help() المبنية مسبقًا في بايثون، والتي تعرض سلسلة التوثيق النصية للكائن الممرر بطريقة أسهل للقراءة من سلاسل __doc__ النصية المباشرة الخام، وهذا يفيد عند التعامل مع الصدفة التفاعلية interactive shell، لأننا نريد الحصول على المعلومات عن أي وحدة أو صنف أو دالة نريد استخدامها. >> from requests import sessions >>> help(sessions) Help on module requests.sessions in requests: NAME requests.sessions DESCRIPTION requests.session ~~~~~~~~~~~~~~~~ This module provides a Session object to manage and persist settings -- More -- إذا كانت سلسة التوثيق النصية أكبر من أن تتسع على الشاشة تعرض بايثون --More-- في أسفل النافذة، ويمكنك الضغط على المفتاح الإدخال ENTER للوصول إلى السطر التالي أو ضغط على مفتاح spacebar للوصول إلى الصفحة التالية أو الضغط على مفتاح Q للخروج من مشاهدة سلسلة التوثيق النصية. ينبغي أن تحتوي سلسلة التوثيق النصية عمومًا على سطر واحد يلخص الوحدة أو الصنف أو الدالة متبوعًا بسطر فارغ ومعلومات مفصلة أكثر، أما بالنسبة للتوابع والدوال، فيمكن أن تحتوي على معلومات عن المُعاملات والقيم المُعادة والآثار الجانبية الخاصة بها. نحن نكتب سلاسل التوثيقات النصية للمبرمجين الآخرين وليس لمستخدمي البرنامج، لذا يجب أن تحتوي معلومات تقنية وليس على دروسًا تعليمية. تقدم سلاسل التوثيق النصية فائدةً ثانيةً مهمة، لأنها تضمّن التوثيقات في الشيفرة المصدرية، فعندما تكتب التوثيقات بصورةٍ منفصلة عن الشيفرة يمكن أن تنساها كاملة، وبدلًا عن ذلك عندما تكتب سلاسل التوثيق النصية في أعلى الوحدات والأصناف والدوال تبقى المعلومات سهلة المراجعة والتحديث. ربما لا تكون قادرًا على كتابة سلاسل التوثيق النصية إذا ما زلت تعمل على الشيفرة التي تريد وصفها، ففي تلك الحالة أضف تعليق TODO في سلسلة التوثيق النصية بمثابة تذكير لإكمال التفاصيل المتبقية. مثلًا لدى الدالة الخيالية reverseCatPolarity() سلسلة توثيق نصية ضعيفة توضّح ما هو واضح أصلًا: def reverseCatPolarity(catId, catQuantumPhase, catVoltage): """عكس قطبية قطّة TODO أنهِ سلسلة التوثيق النصية""" --snip-- ربما تغريك كتابة توثيقات قصيرة والمضي قدمًا لأن كل صنف ودالة وتابع يجب أن يكون لديه سلسلة توثيق نصية، إلا أنه من السهل نسيان أن سلسلة التوثيق النصية هذه ستحتاج لإعادة كتابة دون تعليق TODO. يحتوي PEP257 على توثيق مفصل عن سلاسل التوثيق النصية. ترجمة -وبتصرف- لقسم من الفصل COMMENTS, DOCSTRINGS, AND TYPE HINTS من كتاب Beyond the Basic Stuff with Python. اقرأ أيضًا المقال السابق: البرمجة الوظيفية Functional Programming وتطبيقها في بايثون كيفية كتابة التعليقات في بايثون أساسيات البرمجة بلغة بايثون
-
تمثل البرمجة الوظيفية Functional Programming نموذجًا paradigm يؤكد على كتابة دوال تُجري العمليات الحسابية دون إجراء تعديلات على المتغيرات العامة أو على الحالات خارج الكائنات، مثل الملفات على القرص الصلب أو الاتصالات بالإنترنت أو قواعد البيانات. ويتمحور تصميم بعض لغات البرمجة مثل Erlang و Lisp و Haskell كثيرًا حول مفاهيم البرمجة الوظيفية، أما لغة بايثون ورغم عدم تقيدها بنموذج البرمجة الوظيفية، إلا أنها تحمل بعضًا من ميزاتها، وأهم ما يمكن لبرامج بايثون استخدامه من ميزات البرمجة الوظيفية هي الدوال خالية الآثار الجانبية side-effect-free functions والدوال عالية المستوى higher-order functions والدوال المجهولة lambda functions. يستعرض هذا المقال نموذج البرمجة الوظيفية ومزايا استخدام الدوال وفقًا لهذا النموذج. الآثار الجانبية Side Effects تُعرّف الآثار الجانبية side effects بأنها أي تغييرات تجريها الدالة للأجزاء من البرنامج الواقعة خارج شيفرتها الخاصة ومتغيراتها المحلية، ولتوضيح هذه الفكرة، ننشئ دالة للطرح باسم ()subtract تستخدم عامل الطرح في بايثون (-): >>> def subtract(number1, number2): ... return number1 - number2 ... >>> subtract(123, 987) -864 ليس للدالة ()subtract السابقة آثار جانبية، وذلك لأنها لا تؤثر على أي جزء من البرنامج خارج حدود شيفرتها الخاصة، إذ لا يمكن معرفة ما إذا استُدعيت هذه الدالة سابقًا لمرة أو مرتين أو مليون مرة اعتمادًا على حالة البرنامج أو الحاسوب. قد تعدّل الدالة على متغيراتٍ محلية ضمنها، إلا أن هذه التغيرات تبقى معزولة عن باقي أجزاء البرنامج. لنطلع الآن على الدالة التالية المسماة ()addToTotal، والتي تضيف الوسطاء العددية خاصتها إلى متغير عام باسم TOTAL على النحو التالي: >>> TOTAL = 0 >>> def addToTotal(amount): ... global TOTAL ... TOTAL += amount ... return TOTAL ... >>> addToTotal(10) 10 >>> addToTotal(10) 20 >>> addToTotal(9999) 10019 >>> TOTAL 10019 للدالة ()addToTotal أثر جانبي، وذلك لأنها تعدّل عنصرًا متوجدًا خارجها وهو المتغير العام TOTAL، وقد تكون الآثار الجانبية أكثر من مجرد تغييرات تطرأ على متغير عام، إذ أنها تتضمن تحديث وحذف الملفات أو طباعة النصوص على الشاشة أو فتح اتصال قاعدة بيانات أو المصادقة مع خادم ما أو أي تغييرات تحدث خارج حدود الدالة، إذ يُعد أي أثر يتركه استدعاء الدالة بعد إعادة القيمة أثرًا جانبيًا. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن تشمل الآثار الجانبية أيضًا التغييرات المكانية على الكائنات المتغيرة التي تشير إلى ما هو خارج الدالة. على سبيل المثال، تعدّل الدالة ()removeLastCatFromList التالية وسيط القائمة مكانيًا: >>> def removeLastCatFromList(petSpecies): ... if len(petSpecies) > 0 and petSpecies[-1] == 'cat': ... petSpecies.pop() ... >>> myPets = ['dog', 'cat', 'bird', 'cat'] >>> removeLastCatFromList(myPets) >>> myPets ['dog', 'cat', 'bird'] يحمل كل من المتغير myPets والمعامل petSpecies في المثال السابق مرجعًا إلى نفس القائمة. أي تعديلات مكانية تجري على كائن القائمة ضمن الدالة ستجري أيضًا خارجها، ما يجعل هذا التعديل أثرًا جانبيًا. تُعد الدوال الحتمية deterministic functions أحد المفاهيم ذات الصلة هنا، والتي تعرّف بأنها الدوال التي تعيد دومًا القيمة نفسها من أجل نفس الوسطاء، فمثلًا سيعيد الاستدعاء (subtract(123, 987 دومًا القيمة "864-"، وكذلك تعيد دالة بايثون المبنية مسبقًا ()round (والتي تعمل على تقريب العدد إلى أقرب عدد صحيح) الرقم 3 لدى تمرير العدد 3.14 وسيطًا إليها. لا تعيد الدوال غير الحتمية بالضرورة نفس القيمة من أجل نفس الوسطاء، فعلى سبيل المثال، يعيد الاستدعاء (random.randint(1, 10 قيمةً عشوائيةً محصورةً بين 1 و10، والدالة ()time.time رغم عدم احتوائها على وسطاء إلا أنها تعيد قيمة مختلفة تبعًا للتوقيت الذي تشير إليه ساعة الحاسب لحظة استدعائها؛ ففي حالة الدالة ()time.time، تعد الساعة مصدرًا خارجيًا يمثّل دخلًا للدالة كما يفعل الوسيط. لا تُعد جميع الدوال المعتمدة على مصادر من خارجها، سواء كانت متغيرات عامة، أو ملفات على القرص الصلب، أو قواعد بيانات، أو اتصالات بالإنترنت دوالًا حتمية. إحدى فوائد الدوال الحتمية هي إمكانية التخزين المؤقت لقيمها، فما من ضرورة مثلًا لاستدعاء الدالة ()subtract أكثر من مرة لحساب الفرق بين نفس العددين 123 و987 طالما أنها قادرة على تذكّر القيمة المعادة من المرة الأولى لاستدعائها مع تمرير نفس هذين الوسيطين، وبالتالي تتيح لنا الدوال الحتمية إمكانية المفاضلة ما بين الزمن والمساحة التخزينية، بمعنى زيادة سرعة تنفيذ دالة على حساب المساحة التخزينية اللازمة في الذاكرة لتخزين النتائج السابقة لهذه الدالة. وتدعى الدالة الحتمية خالية الآثار الجانبية بالدالة النقية pure function. تسعى البرمجة الوظيفية لإنشاء دوال نقية فقط في برامجها. إذ توفّر الدوال النقية العديد من المزايا، ومنها: مناسبة تمامًا لاختبار وحدة مستقلةً، إذ أنها لا تتطلب إعداد أي مصادر خارجية. يسهل في الدوال النقية إعادة الحصول على الخطأ نفسه باستدعائها من أجل نفس الوسطاء، لفهم أسباب الخطأ وإصلاحه. الدوال النقية قادرة على استدعاء دوال نقية أخرى مع بقائها نقية. تكون الدوال النقية في البرامج متعددة المستخدمين المتزامنين multithreaded programs آمنة من الخيوط thread-safe، إذ يمكن تشغيلها في وقتٍ واحد بأمان. موضوع البرامج متعددة المستخدمين المتزامنين multithreaded programs خارج اهتمامات مقالنا هذا. يمكن إجراء استدعاءات متعددة متزامنة وبأي ترتيب للدوال النقية من قبل نوى وحدة المعالجة المركزية CPU المتوازية، أو برنامج متعدد مستخدمين كونها لا تعتمد على أي مصدر خارجي يفرض تشغيلها بترتيب معين. يمكنك، لا بل ينبغي عليك كتابة دوال نقية في بايثون متى استطعت ذلك. بُنيت دوال بايثون لتكون نقيةً بحكم العرف والعادة ولكن ما من إعداد معيّن يجعل مفسر بايثون يفرض على الدوال أن تكون نقية، ولعل الطريقة الأكثر شيوعًا لجعل الدوال نقية تكون بتجنب استخدام المتغيرات العامة فيها والتأكد من كون الدوال لا تتعامل مع الملفات، أو الإنترنت، أو ساعة النظام، أو الأعداد العشوائية، أو غيرها من المصادر الخارجية. الدوال عالية المستوى Higher-Order Functions يمكن للدوال عالية المستوى Higher-Order Functions استقبال الدوال الأخرى مثل وسطاء لها، أو أن تعيد دوال أخرى مثل قيمة معادة، فعلى سبيل المثال، لنعرّف دالة باسم ()callItTwice تعمل على استدعاء أي دالة أخرى مرتين: >>> def callItTwice(func, *args, **kwargs): ... func(*args, **kwargs) ... func(*args, **kwargs) ... >>> callItTwice(print, 'Hello, world!') Hello, world! Hello, world! تعمل الدالة ()callItTwice السابقة مع أي دالة تمرر إليها، إذ تعد الدوال في بايثون كائنات من الدرجة الأولى، بمعنى أنها كغيرها من الكائنات من الممكن تخزينها ضمن متغيرات أو تمريرها مثل وسطاء أو استخدامها مثل قيم معادة. الدوال المجهولة anonymous functions الدوال لامدا lambda functions أو الدوال المجهولة anonymous functions أو الدوال عديمة الاسم nameless functions هي دوال بسيطة عديمة الأسماء وتتألف شيفرتها من تعليمة return فقط، وتُستخدم عادةً الدوال المجهولة لدى تمرير الدوال مثل وسطاء لدول أخرى، فعلى سبيل المثال، من الممكن إنشاء دالة عادية تستقبل قائمة متضمنة لطول وعرض مستطيل بأبعاد 10 و4 على النحو التالي: >>> def rectanglePerimeter(rect): ... return (rect[0] * 2) + (rect[1] * 2) ... >>> myRectangle = [4, 10] >>> rectanglePerimeter(myRectangle) 28 فتبدو الدالة المجهولة المكافئة كما يلي: lambda rect: (rect[0] * 2) + (rect[1] * 2) نستخدم الكلمة المفتاحية lambda للتصريح عن دالة مجهولة في بايثون متبوعةً بقائمة بالمعاملات (في حال وجودها) مفصولةً فيما بينها بمحارف فاصلة، ومن ثم نقطتين رأسيتين ونهايةً تعبير برمجي يُمثّل القيمة المعادة. بما أن الدوال هي كائنات من الدرجة الأولى في بايثون، يمكن إسناد الدالة المجهولة إلى متغير ما، على غرار ما تؤديه التعليمة def، على النحو التالي: >>> rectanglePerimeter = lambda rect: (rect[0] * 2) + (rect[1] * 2) >>> rectanglePerimeter([4, 10]) 28 أسندنا في الشيفرة السابقة الدالة المجهولة إلى متغير اسمه rectanglePerimeter، ما يعطي بالنتيجة دالةً باسم ()rectanglePerimeter، وبذلك نجد أن الدوال المُنشأة باستخدام التعليمة lambda تماثل تلك المُنشأة باستخدام التعليمة def. ملاحظة: يفضل في الشيفرات الواقعية استخدام التعليمة def على إسناد دالة مجهولة إلى متغير، إذ أن الغرض الأساسي من وجود الدوال المجهولة هو استخدامها في الحالات التي لا تحتاج فيها الدالة إلى اسم. صياغة الدوال المجهولة مفيدة في تخصيص دوال صغيرة لتعمل مثل وسطاء عند استدعاء دوال أخرى. على سبيل المثال، تمتلك الدالة ()sorted وسيطًا مسمى يدعى key يسمح بتحديد دالة، فبدلًا من فرز العناصر في قائمة ما وفقًا لقيمها، تفرزها وفقًا للقيمة المعادة من تلك الدالة الممررة إلى الوسيط key. مررنا في المثال التالي دالةُ مجهولةً إلى الدالة ()sorted تعيد محيط مستطيل مُعطى الأبعاد، ما يجعل الدالة ()sorted تفرز العناصر اعتمادًا على المحيط المحسوب من طوله وعرضه [width, height] الواردان في القائمة، بدلًا من فرزها اعتمادًا على قيم الطول والعرض نفسها، على النحو التالي: >>> rects = [[10, 2], [3, 6], [2, 4], [3, 9], [10, 7], [9, 9]] >>> sorted(rects, key=lambda rect: (rect[0] * 2) + (rect[1] * 2)) [[2, 4], [3, 6], [10, 2], [3, 9], [10, 7], [9, 9]] بدلًا من فرز القيم [10,2] أو [3,6] مثلًا، أصبحت الدالة تفرزها اعتمادًا على قيمة المحيط المعادة المتمثلة بالأعداد الصحيحة 24 و18 على التوالي. تعد الدوال المجهولة اختصارًا مناسبًا للصياغة، إذ من الممكن تعريف دالة مجهولة صغيرة مؤلفة من سطر برمجي واحد، بدلًا من تعريف دالة مسماة جديدة باستخدام التعليمة def. الربط والترشيح باستخدام بنى اشتمال القوائم List Comprehensions كانت الدالتين ()map و ()filter في الإصدارات الأقدم من بايثون من الدوال عالية المستوى الشائعة القادرة على ربط القوائم وترشيحها، الأمر الذي كان يجري غالبًا بمساعدة الدوال المجهولة. تستطيع عملية الربط Mapping إنشاء قائمة قيم اعتمادًا على قيم قائمة أخرى؛ بينما ينشئ الترشيح قائمةً تتضمن فقط القيم التي تحقق معيارًا معينًا من قائمة أخرى. على سبيل المثال، لو أردنا إنشاء قائمة جديدة تتضمن قيم من نوع السلاسل النصية بدلًا من بدلًا من الأعداد الصحيحة في القائمة التالية [7 ,6 ,1 ,12 ,19 ,18 ,16 ,8]، فمكن الممكن تمرير كل من القائمة هذه والتابع المجهول (lambda n: str(n إلى دالة الربط ()map، على النحو التالي: >>> mapObj = map(lambda n: str(n), [8, 16, 18, 19, 12, 1, 6, 7]) >>> list(mapObj) ['8', '16', '18', '19', '12', '1', '6', '7'] تعيد الدالة ()map كائنًا من النوع map، والذي يمكن تحويله إلى قائمة بتمريره إلى الدالة ()list، وبذلك تتضمن القائمة المربوطة الآن قيمًا من نوع سلاسل محرفية موافقة للأعداد الصحيحة الموجودة في القائمة الأصلية. تعمل دالة الترشيح ()filter بآلية مشابهة، إلا أن وسيط الدالة المجهولة في هذه الحالة يحدد العناصر من القائمة التي ستبقى (عند إعادة الدالة المجهولة للقيمة True من أجل هذا العنصر) وتلك التي ستُرشّح (عند إعادة الدالة المجهولة للقيمة False من أجل هذا العنصر). على سبيل المثال، يمكن تمرير الدالة المجهولة lambda n: n % 2 == 0 لترشيح أي أعداد فردية في السلسلة على النحو التالي: >>> filterObj = filter(lambda n: n % 2 == 0, [8, 16, 18, 19, 12, 1, 6, 7]) >>> list(filterObj) [8, 16, 18, 12, 6] تعيد الدالة ()filter كائن ترشيح من النوع filter، والذي يمكن أيضًا تمريره إلى الدالة ()list، وبذلك تبقى الأعداد الزوجية فقط في القائمة بعد الترشيح. تمثل الدالتان ()map و ()filter طرقًا قديمة في إنشاء قوائم مربوطة أو مُرشّحة في بايثون. فالآن أصبح من الممكن إنشاء هذه القوائم باستخدام بنى اشتمال القوائم، والتي لا تتطلب كتابة دوال مجهولة ناهيك عن سرعتها مقارنةً بالدالتين ()map و ()filter. سنعيد فيما يلي مثال الدالة ()map ولكن باستخدام بنية اشتمال قوائم: >>> [str(n) for n in [8, 16, 18, 19, 12, 1, 6, 7]] ['8', '16', '18', '19', '12', '1', '6', '7'] نلاحظ أن الجزئية (str(n من بنية اشتمال القوائم في الشيفرة أعلاه تشابه في وظيفتها الدالة المجهولة (lambda n: str(n من الطريقة السابقة. وفيما يلي نعيد مثال الدالة ()filter ولكن باستخدام بنية اشتمال قوائم: >>> [n for n in [8, 16, 18, 19, 12, 1, 6, 7] if n % 2 == 0] [8, 16, 18, 12, 6] نلاحظ أن الجزئية n % 2 == 0 من بنية اشتمال القوائم في الشيفرة أعلاه تشابه في وظيفتها الدالة المجهولة lambda n: n % 2 == 0 من الطريقة السابقة. تتعامل العديد من اللغات مع مفهوم الدوال على أنها كائنات من الدرجة الأولى، ما يسمح بوجود دوال عالية المستوى بما يتضمن دوال الربط والترشيح. ينبغي على القيم المعادة أن تتضمن دوما نمط البيانات نفسه تعد بايثون لغة برمجة ديناميكية الأنماط، ما يعني أن توابع بايثون ودوالها تمتلك حرية إعادة قيم من أي نمط بيانات، ولكن وبغية تجنب السلوكيات غير المتوقعة للدوال، ينبغي أن نسعى لجعلها تعيد قيمًا من نمط بيانات واحد فقط. على سبيل المثال، لدينا في الشيفرة التالية دالة تعتمد على رقم عشوائي لتعيد إما عددًا صحيحًا أو سلسلةً نصية: >>> import random >>> def returnsTwoTypes(): ... if random.randint(1, 2) == 1: ... return 42 ... else: ... return 'forty two' لدى كتابة شيفرة تستدعي هذه الدالة، سيكون من السهل نسيان وجوب التعامل مع عدة أنماط بيانات ممكنة. واستكمالًا لهذا المثال، لنفرض أننا سنستدعي الدالة ()returnsTwoTypes ونريد تحويل العدد الذي تعيده إلى نظام العد السداسي عشري على النحو التالي: >>> hexNum = hex(returnsTwoTypes()) >>> hexNum '0x2a' تعيد دالة بايثون ()hex المبنية مسبقًا سلسلةً نصيةً لقيمة العدد الصحيح الممرر إليها في نظام العد السداسي عشري. ستعمل الشيفرة السابقة على نحو سليم طالما أن الدالة ()returnsTwoTypes تعيد قيمةً عدديةً صحيحة، ما يوحي بأن الشيفرة خالية الأخطاء، إلا أنه بمجرد إعادة الدالة ()returnsTwoTypes لسلسلة نصية، سيظهر الاستثناء التالي: >>> hexNum = hex(returnsTwoTypes()) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object cannot be interpreted as an integer فمن الضروري أن نتذكر دومًا ضرورة التعامل مع كافة أنماط البيانات التي قد تعيدها الدالة، ولكن في الواقع من السهل نسيان هذا الأمر. لتجنب هذا النوع من الأخطاء، علينا أن نحاول دومًا جعل القيم المعادة من الدوال تعود لنمط بيانات واحد، ولا يمكن عد ذلك توجيهًا صارمًا، ففي بعض الحالات لا مفر من جعل الدالة تعيد قيمًا من أنماط بيانات مختلفة، ولكن إجمالًا كلما كانت الدوال أقرب لإعادة قيم من نمط بيانات واحد، كلما كانت أسهل وأقل عرضةً للأخطاء. توجد حالة عملية ينبغي الانتباه إليها وهي ألا نجعل الدالة تعيد القيمة None إلا إذا كانت لا تعيد سواها، إذ أن القيمة None هي الوحيدة ضمن نمط البيانات None، فقد نرغب بجعل الدالة تعيد None للدلالة على حدوث خطأ ما (الأمر الذي سنناقشه في الفقرة التالية "إظهار الاستثناءات مقابل إعادة رموز الأخطاء")، ولكن عليك حصر استخدام None فقط مثل قيمة معادة للدوال التي لا تمتلك أي قيمة معادة ذات معنى، والسبب في ذلك هو أن إعادة القيمة None للدلالة على وقوع خطأ يعد مصدرًا شائعًا للاستثناء غير المعلوم الخاص بنمط البيانات None والدال على استخدام سمة ليست من سمات الكائن 'NoneType' object has no attribute، كما في المثال: >>> import random >>> def sometimesReturnsNone(): ... if random.randint(1, 2) == 1: ... return 'Hello!' ... else: ... return None ... >>> returnVal = sometimesReturnsNone() >>> returnVal.upper() 'HELLO!' >>> returnVal = sometimesReturnsNone() >>> returnVal.upper() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'NoneType' object has no attribute 'upper' رسالة الخطأ السابقة غامضة نسبيًا، وقد تتطلب بعض الجهد لتتبع سببها عودةً إلى دالة تعيد في الأحوال الطبيعية قيمة متوقعة ولكنها قد تعيد القيمة None في حال وقوع خطأ، وقد وقع الخطأ في مثالنا السابق لأن الدالة ()sometimesReturnsNone أعادت القيمة None والتي أسندناها إلى المتغير returnVal. في حين أن رسالة الخطأ توحي بأن الخطأ حصل في استدعاء التابع ()upper. إظهار الاستثناءات Exceptions مقابل إعادة رموز الأخطاء Error Codes للمصطلحين استثناء exception وخطأ error نفس المعنى تقريبًا في بايثون وهو: ظرف أو حالة استثنائية في البرنامج تشير عادةً إلى وجود مشكلة. أصبحت الاستثناءات شائعة وبمثابة ميزة للغات البرمجة في الثمانينات والتسعينات وذلك في لغتي C++ وجافا، إذ حلت الاستثناءات محل استخدام رموز الأخطاء والتي تمثّل القيم المعادة من الدالة لتشير إلى وجود مشكلة. لعل فائدة الاستثناءات تكمن في كونها تعيد قيمًا متعلقة بعمل الدالة نفسه بدلًا من الإشارة إلى وجود خطأ فحسب. قد تسبب رموز الأخطاء بحد ذاتها مشاكلًا في البرنامج، فعلى سبيل المثال، تعيد الدالة ()find الخاصة بالتعامل مع السلاسل النصية في بايثون عادةً الفهرس الموافق لمكان وجود سلسلة نصية فرعية ضمن سلسلة نصية رئيسية ما، وفي حال عدم عثورها على السلسلة الفرعية المطلوبة، فإنها تعيد القيمة "1-" مثل رمز خطأ، ولكن وبما أننا قد نستخدم القيمة "1-" للدلالة على رقم فهرس محرف ابتداءً من نهاية السلسلة النصية، فقد يؤدي استخدام "1-" رمزًا لخطأ إلى وقوع خطأ غير مقصود. لنكتب ما يلي ضمن الصدفة التفاعلية كمثال: >>> print('Letters after b in "Albert":', 'Albert'['Albert'.find('b') + 1:]) Letters after b in "Albert": ert >>> print('Letters after x in "Albert":', 'Albert'['Albert'.find('x') + 1:]) Letters after x in "Albert": Albert تقيّم الشيفرة السابقة الجزئية ('Albert'.find('x' إلى رمز الخطأ "1-" (نظرًا لعدم وجود المحرف x ضمن السلسة النصية Albert)، وهذا ما يجعل بدوره التعبير البرمجي [:Albert'['Albert'.find('x') + 1' يُقيّم إلى [:Albert'[-1 + 1' والذي يُقيّم هو الآخر إلى [:Albert'[0' وبالتالي إلى القيمة 'Albert'. لا تمثّل هذه النتيجة تلك المقصودة من الشيفرة، إذ سيظهر استدعاء التابع ()index بدلًا من ()find كما في [:Albert'['Albert'.index('x') + 1' استثناءً، ما يجعل المشكلة جليةً وواضحة. يعيد التابع ()index الخاص بالسلاسل النصية من جهة أخرى استثناء خطأ القيمة ValueError في حال عدم العثور على الدالة الفرعية، وإذا لم نتعامل مع هذا الاستثناء، سيتوقف البرنامج عن العمل، الأمر الأفضل من عدم ملاحظة وجود خطأ أصلًا. تنتهي عادةً أسماء أصناف الاستثناءات بالكلمة "Error" وذلك عندما يشير الاستثناء إلى خطأ فعلي، مثل ValueError أو NameError للدلالة على خطأ في الاسم أو SyntaxError للدلالة على خطأ صياغي، أما أصناف الاستثناءات التي تشير إلى حالات استثنائية والتي لا تمثل أخطاء بالضرورة فتتضمن StopIteration أو KeyboardInterrupt أو SystemExit. الخلاصة رغم كون بايثون ليست بلغة برمجة وظيفية، إلا أنها تمتلك العديد من الميزات التي تستخدمها هذه اللغات، إذ أن الدوال في بايثون هي كائنات من الدرجة الأولى، ما يعني إمكانية تخزينها ضمن متغيرات وتمريرها مثل وسطاء لدوال أخرى (إذ تدعى تلك الأخيرة بالدوال عالية المستوى)، كما توفر الدوال المجهولة اختصارًا في الصياغة لحالات الحاجة لاستخدام دوال عديمة الاسم ومجهولة مثل وسطاء لدوال عالية المستوى. لعل دالة الربط ()map ودالة الترشيح ()filter من أشهر الدوال عالية المستوى في بايثون، رغم إمكانية تنفيذ نفس وظيفتهما على نحوٍ أسرع بالاعتماد على بنى اشتمال القوائم. ينبغي أن تنتمي القيم المعادة من الدالة دومًا إلى نفس نمط البيانات، كما ينبغي عليك تجنب استخدام رموز الأخطاء مثل قيم معادة، إذ تُمثّل القيمة None إحدى القيم المُستخدمة على نحو خاطئ على أنها رمزٌ للخطأ. ترجمة -وبتصرف- للجزء الثاني من الفصل العاشر "كتابة دوال فعالة في بايثون" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: كتابة دوال فعالة في بايثون مقدمة إلى البرمجة الوظيفية Functional Programming البرمجة كائنية التوجه
-
تعدّ الدوال functions برامجًا صغيرةً موجودةً ضمن البرنامج الأساسي، سامحةً لنا بتجزئة الشيفرة إلى وحداتٍ أصغر، كما تجنبنا عناء كتابة شيفراتٍ مُكررة، والتي قد تتسبب بحدوث الأخطاء، إلا أن كتابة دوال فعالة يتطلب اتخاذ العديد من القرارات حول اسم الدالة وحجمها ومعاملاتها ومدى تعقيدها. يستعرض هذا المقال الطرق المختلفة لكتابة الدوال، موضحًا مزايا وعيوب المفاضلات المختلفة، إذ سنتعمق في كيفية المفاضلة ما بين الدوال الصغيرة والكبيرة، وفي كيفية تأثير عدد المعاملات على تعقيد الدالة، وفي كيفية كتابة دوال باستخدام أعداد متغيرة من الوسطاء باستخدام العاملين * و **. أسماء الدوال تتبع أسماء الدوال عمومًا الاصطلاحات نفسها المُستخدمة في تسمية المعرفات بأنواعها، إلا أن اسم الدالة يتضمن عادةً صيغةً فعليةً، إذ أنها تُنفذ أمرًا ما غالبًا، كما يمكن تضمين صيغة اسمية لتوصيف ما تفعله الدالة؛ فعلى سبيل المثال، تدل أسماء دوال مثل ()refreshConnection على تحديث الإتصال و ()setPassword على تعيين كلمة مرور و ()extract_version على استخراج رقم الإصدار، ويوضّح كل منها ما تفعله الدالة ولماذا تفعله. قد لا نحتاج لصيغة اسمية ضمن أسماء التوابع المُنشأة على أنها جزء من صنف أو وحدة ما، مثل تابع باسم ()reset من ضمن صنف باسم SatelliteConnection أو الدالة ()open من الوحدة webbrowser، إذ يوفر في هذه الحالة اسم الصنف أو الوحدة المعلومات اللازمة لفهم سياق عمل الدالة أو التابع؛ فمن الواضح في المثال السابق أن التابع ()reset يعيد تعيين الاتصال عبر الأقمار الصناعية satellite connection وأن الدالة ()open تفتح متصفح الويب. يُفضّل استخدام أسماء طويلة تؤدي المعنى على أسماء مختصرة وقصيرة جدًا، إذ سيفهم المختص بالرياضيات مباشرةً أن دالةً باسم ()gcd ستعيد القاسم المشترك الأكبر لعددين، أما بالنسبة لغير المختصين، فسيجدون الاسم ()getGreatestCommonDenominator أوضح. وتذكّر ألا تستخدم أي من أسماء الدوال أو الوحدات المبنية مسبقًا في بايثون لتسمية دوالك، مثل: all any date email file format hash id input list min max object open random set str sum test type دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن مفاضلات أحجام الدوال يرى بعض المبرمجين أنه على الدوال أن تكون أقصر ما يمكن وألا تتجاوز ما يمكن عرضه على شاشة واحدة دون الحاجة إلى التمرير لأسفل؛ فالدالة المكونة من اثني عشر سطر برمجي ستكون سهلة للفهم نسبيًا، على الأقل لدى مقارنتها بأخرى مكونة من مئات الأسطر البرمجية، لكن قد ينطوي جعل الدوال أقصر عن طريق تجزئتها إلى عدة دوال فرعية صغيرة على عيوب أيضًا. لنتعرف بدايةً على فوائد الدوال الصغيرة: شيفرة الدالة أسهل للفهم. تحتاج الدالة غالبًا إلى عدد أقل من المعاملات. احتمالية أن يكون للدالة آثار سلبية جانبية أقل كما هو موضح في الفقرة "البرمجة الوظيفية" من هذا المقال. سيكون اختبار الدالة وتنقيحها أسهل. ستعرض الدالة غالبًا أنواع مختلفة أقل من الاستثناءات. تنطوي الدوال القصيرة أيضًا على بعض العيوب ومنها: يعني استخدام دوال قصيرة غالبًا الحاجة لاستخدام عدد أكبر من الدوال في البرنامج. يجعل استخدام عدد أكبر من الدوال البرنامج أعقد. يعني استخدام المزيد من الدوال الحاجة لابتكار المزيد من الأسماء الدقيقة التوصيفية ذات المعنى، وهي مهمة ليست بالسهلة. ينطوي استخدام المزيد من الدوال على الحاجة لكتابة المزيد من التوثيقات والتعليقات. تصبح العلاقات بين الدوال أعقد. يتمسّك البعض بالمبدأ التوجيهي القائل: "الأقصر أفضل" بمبالغة، مفترضين أنه على كل دالة أن تكون بحدود ثلاثة أو أربعة أسطر برمجية على الأكثر، وهذا منافٍ للمنطق، فعلى سبيل المثال، لنأخذ دالة قراءة انتقالات اللاعب ()getPlayerMove المستخدمة في بناء لعبة برج هانوي لاحقًا، ولا يهمنا الآن فهم كيفية عمل هذه الشيفرات، وإنما الهدف منها فقط الاطلاع على الهيكلية العامة لهذه الدالة: def getPlayerMove(towers): """Asks the player for a move. Returns (fromTower, toTower).""" while True: # الاستمرار بالطلب من المستخدم حتى إدخال انتقال صحيح print('Enter the letters of "from" and "to" towers, or QUIT.') print("(e.g. AB to moves a disk from tower A to tower B.)") print() response = input("> ").upper().strip() if response == "QUIT": print("Thanks for playing!") sys.exit() # التأكّد من أنّ المستخدم قد أدخل أحرفًا صحيحة لاسم للبرج if response not in ("AB", "AC", "BA", "BC", "CA", "CB"): print("Enter one of AB, AC, BA, BC, CA, or CB.") continue # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا # استخدام أسماء ذات توصيفية أعلى fromTower, toTower = response[0], response[1] if len(towers[fromTower]) == 0: # لا يمكن أن يكون فارغًا "from" برج البداية print("You selected a tower with no disks.") continue # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا elif len(towers[toTower]) == 0: # فارغ "to" يمكن نقل أي قرص إلى برج وجهة return fromTower, toTower elif towers[toTower][-1] < towers[fromTower][-1]: print("Can't put larger disks on top of smaller ones.") continue # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا else: # الانتقال صحيح، لذا سنعيد اسم برج البداية وبرج الوجهة المُحددان return fromTower, toTower تمتد الدالة السابقة على 34 سطر برمجي، ورغم أنها تغطي عدّة مهام تتضمن السماح للّاعب بإجراء انتقال جديد والتحقق من صحة هذا الانتقال والطلب منه اختيار انتقال آخر في حال كون السابق غير مسموح، إلا أن كل من المهام السابقة تندرج تحت بند قراءة انتقالات اللاعب، وفي حال كنا مصممين على كتابة دوال قصيرة، فمن الممكن تجزئة شيفرات الدالة ()getPlayerMove إلى دوال أصغر، على النحو التالي: def getPlayerMove(towers): """Asks the player for a move. Returns (fromTower, toTower).""" while True: # الاستمرار بالطلب من المستخدم حتى إدخال انتقال صحيح response = askForPlayerMove() terminateIfResponseIsQuit(response) if not isValidTowerLetters(response): continue # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا # استخدام أسماء ذات توصيفية أعلى fromTower, toTower = response[0], response[1] if towerWithNoDisksSelected(towers, fromTower): continue # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا elif len(towers[toTower]) == 0: # فارغ "to" يمكن نقل أي قرص إلى برج وجهة return fromTower, toTower elif largerDiskIsOnSmallerDisk(towers, fromTower, toTower): continue # الطلب من المستخدم لإدخال الانتقال المطلوب مجددًا else: # الانتقال صحيح، لذا سنعيد اسم برج البداية وبرج الوجهة المُحددان return fromTower, toTower def askForPlayerMove(): """Prompt the player, and return which towers they select.""" print('Enter the letters of "from" and "to" towers, or QUIT.') print("(e.g. AB to moves a disk from tower A to tower B.)") print() return input("> ").upper().strip() def terminateIfResponseIsQuit(response): """Terminate the program if response is 'QUIT'""" if response == "QUIT": print("Thanks for playing!") sys.exit() def isValidTowerLetters(towerLetters): """Return True if `towerLetters` is valid.""" if towerLetters not in ("AB", "AC", "BA", "BC", "CA", "CB"): print("Enter one of AB, AC, BA, BC, CA, or CB.") return False return True def towerWithNoDisksSelected(towers, selectedTower): """Return True if `selectedTower` has no disks.""" if len(towers[selectedTower]) == 0: print("You selected a tower with no disks.") return True return False def largerDiskIsOnSmallerDisk(towers, fromTower, toTower): """Return True if a larger disk would move on a smaller disk.""" if towers[toTower][-1] < towers[fromTower][-1]: print("Can't put larger disks on top of smaller ones.") return True return False تمتد الدوال الستة السابقة على 56 سطر، أي بحدود ضعف ما كانت عليه شيفرة الدالة الأصلية مع أدائها للمهام ذاتها، ورغم أن كل دالة من الدوال الستة أسهل للفهم وحدها من فهم الدالة ()getPlayerMove الأصلية كاملةً، إلا أن تجميع هذه الدوال معًا يزيد من التعقيد، فقد يواجه قراء شيفرة كهذه صعوبةً في فهم كيفية توافق هذه الدوال مع بعضها بعضًا، كما أن الدالة ()getPlayerMove من بين الدوال الستة هي الوحيدة التي ستُستدعى في باقي أجزاء البرنامج، في حين أن الدوال الخمسة المُتبقية لن تُستدعى سوى لمرة واحدة ومن قِبل الدالة ()getPlayerMove نفسها، إلا أنّ كتلة الدوال السابقة لا تعبّر بضخامتها عن هذه الحقيقة، ناهيك عن الحاجة لابتكار أسماء وسلاسل توثيق نصية docstrings جديدة (السلسلة النصية المحصورة بين علامات اقتباس ثلاثية أسفل كل تعليمة تصريح عن دالة def) لكل دالة جديدة، والذي يؤدي إلى وجود أسماء متشابهة تسبب الارتباك، مثل الدالتين ()getPlayerMove و ()askForPlayerMove. لا تزال الدالة ()getPlayerMove الجديدة بعد التقسيم تتجاوز ثلاث أو أربع أسطر برمجية، فلو كنا نتبع المبدأ التوجيهي "الأقصر أفضل" بحرفيته لكنا مضطرين لتقسيمها إلى المزيد من التوابع الفرعية الأصغر؛ ففي مثل هذه الحالة، قد تؤدي سياسة استخدام دوال قصيرة جدًا إلى الحصول على دوال أبسط، إلا أن التعقيد الإجمالية للبرنامج سيزداد جذريًا. وفقًا لرأي صاحب المقال: يجب ألا تتجاوز التوابع في الحالة المثالية 30 سطرًا برمجيًا، وما عدا ذلك ألا تتجاوز طبعًا حدود 200 سطر برمجي. اجعل دوالك أقصر ما يمكن ضمن حدود الممكن والمعقول وليس أكثر. معاملات ووسطاء الدوال تُعرف معاملات الدالة Function Parameters بأنها أسماء المتغيرات الموجودة بين قوسي الدالة ضمن تعليمة def المسؤولة عن التصريح عن الدالة، في حين أن وسطاء الدالة Function Arguments هي القيم الممررة بين قوسي الدالة عند استدعائها. وكلما زاد عدد معاملات الدالة، زادت إمكانية ضبط وتعميم شيفرتها، ولكن في الوقت نفسه زاد تعقيدها. لعل إحدى القواعد الجيدة لاتباعها في هذا الصدد تنص على أن استخدام من 0 إلى 3 معاملات هو أمر مناسب، ولكن ما يزيد عن 5 أو 6 معاملات يعد غالبًا أكثر من اللازم؛ فبمجرد أن تصبح الدالة شديدة التعقيد، من الأفضل التفكير بكيفية تقسيمها إلى دوال أصغر بعدد معاملات أقل لكل منها. الوسطاء الافتراضية Default Arguments تتمثل إحدى طرق تقليص تعقيد معاملات الدالة في كتابة الوسيط الافتراضي للمعاملات، إذ يُعرّف الوسيط الافتراضي بأنه قيمة تُستخدم وسيطًا للدالة في حال استدعائها دون تمرير وسيط إليها؛ فعند استخدام معظم استدعاءات الدالة قيمةً محددةً للمعامل، نجعل هذه القيمة وسيطًا افتراضيًا مُتجنبين إدخالها عدة مرات عند كل استدعاء للدالة. نحدد وسيطًا افترضيًا ضمن تعليمة def بكتابته عقب اسم المعامل وإشارة مساواة. على سبيل المثال، في الدالة ()introduction التالية، للمعامل المسمى greetings القيمة Hello وهي قيمة افتراضية تُستخدم في حال عدم تحديد قيمة له لدى استدعاء الدالة: >>> def introduction(name, greeting='Hello'): ... print(greeting + ', ' + name) ... >>> introduction('Alice') Hello, Alice >>> introduction('Hiro', 'Ohiyo gozaimasu') Ohiyo gozaimasu, Hiro عند استدعاء الدالة ()introduction دون تمرير قيمة للوسيط الثاني فيها، تستخدم السلسلة النصية Hello افتراضيًا. نلاحظ أن المعاملات ذات الوسطاء الافتراضية تأتي دومًا بعد تلك التي بدون وسطاء افتراضية. يمكنك العودة إلى مقال البنى الصحيحة المؤدية إلى الأخطاء الشائعة في بايثون الذي وضحنا فيه أهمية عدم استخدام الكائنات المتغيّرة mutable objects مثل القائمة الفارغة [] أو القاموس الفارغ {} على أنها قيم افتراضية وذلك ضمن الفقرة "لا تستخدم القيم المتغيرة من أجل وسطاء افتراضية"، إذ بيّنا فيها المشكلة التي تتسبب بها هذه المنهجية وكيفية حلها. استخدام * و ** لتمرير الوسطاء إلى الدوال من الممكن استخدام الصيغة * أو ** (وتُلفظ نجمة star ونجمة نجمة star star على التوالي) لتمرير مجموعة من الوسطاء إلى الدوال مثل قيم منفصلة؛ إذ نستخدم الصيغة * لتمرير العناصر ضمن كائن تكراري، مثل القائمة أو الصف؛ أما الصيغة ** فتسمح بتمرير أزواج مفتاح-قيمة في كائن مفهرس بالمفاتيح mapping object مثل القاموس بمثابة وسطاء منفصلة. يمكن على سبيل المثال تمرير عدة وسطاء إلى الدالة ()print، فتوضع مسافات فيما بينها افتراضيًا، كما هو مُبيّن في الشيفرة التالية: >>> print('cat', 'dog', 'moose') cat dog moose تدعى هذه الوسطاء بالوسطاء الموضعية positional، إذ يحدد موقعها ضمن استدعاء الدالة الوسيط المُحدد لكل معامل. ولكن ماذا لو خُزّنت السلاسل النصية هذه ضمن قائمة وحاولنا تمرير القائمة كاملةً إلى الدالة؟ ستعتقد الدالة ()print أنك ترغب بطباعة السلسلة كاملةً على أنها قيمة واحدة على النحو التالي: >>> args = ['cat', 'dog', 'moose'] >>> print(args) ['cat', 'dog', 'moose'] نلاحظ أن تمرير القائمة إلى الدالة ()print يطبع القائمة كما هي، بما في ذلك الأقواس المعقوفة وعلامات الاقتباس ومحارف الفاصلة. وإحدى طرق طباعة عناصر القائمة منفردةً تكون بتجزئة القائمة إلى عدة وسطاء بتمرير فهرس كل عنصر إلى الدالة على حدى، ما يجعل الشيفرة أصعب للقراءة: >>> # مثال عن شيفرة أصعب للقراءة >>> args = ['cat', 'dog', 'moose'] >>> print(args[0], args[1], args[2]) cat dog moose توجد طريقة أسهل لتمرير هذه العناصر إلى الدالة ()print، إذ يمكن استخدام الصيغة * لتفسير العناصر ضمن قائمة أو أي نمط بيانات تكراري على أنها وسطاء موضعية منفردة ضمن الدالة. لنكتب المثال التالي في الصدفة التفاعلية: >>> args = ['cat', 'dog', 'moose'] >>> print(*args) cat dog moose تتيح الصيغة * إمكانية تمرير عناصر القائمة إلى الدالة مثل قيم مفردة بغض النظر عن عدد العناصر في القائمة، كما يمكن استخدام الصيغة ** لتمرير أنماط البيانات المفهرسة بالمفاتيح، مثل القواميس على أنها وسطاء مسماة Keyword argument مستقلة، إذ تُسبق الوسطاء المسماة باسم المعامل وإشارة مساواة. على سبيل المثال، لدى الدالة ()print وسيطًا مسمى يدعى sep والذي يحدد سلسلةً نصيةً لتوضع ما بين الوسطاء التي ستُعرض، وتُعين افتراضيًا لتكون مسافةً فارغة ' '. يمكن إسناد قيمة جديدة للوسيط المسمى وذلك إما باستخدام تعليمة إسناد أو الصيغة **. لنكتب ما يلي في الصدفة التفاعلية مثلًا: >>> print('cat', 'dog', 'moose', sep='-') cat-dog-moose >>> kwargsForPrint = {'sep': '-'} >>> print('cat', 'dog', 'moose', **kwargsForPrint) cat-dog-moose نلاحظ أن التعليمات السابقة تعطي خرجًا متماثلًا، إذ استخدمنا في المثال سطرًا برمجيًا واحدًا لإعداد القاموس kwargsForPrint المتضمن قيمة الوسيط المسمى الخاص بالدالة ()print، ولكن قد نحتاج في الحالات الأعقد للمزيد من الشيفرات لدى إعداد قاموس بالوسطاء المسماة. تتيح الصيغة ** لنا إمكانية إنشاء قاموس مخصص بإعدادات الضبط لنمرره عند استدعاء الدالة، وهذا أمر مفيد لا سيما للدوال والتوابع ذات العدد الكبير من الوسطاء المسماة. بذلك نجد بأنه مع استخدام صيغتي * و **، مع تعديل قائمة أو قاموس أثناء التنفيذ، يمكن تزويد الدالة عند استدعائها بعددٍ متغير من الوسطاء. استخدام * لإنشاء دوال مرنة Variadic Functions يمكن أيضًا استخدام الصيغة * ضمن تعليمة التصريح عن الدوال def بغية إنشاء دوال مرنة تستقبل عددًا متغيرًا من الوسطاء الموضعيين. على سبيل المثال، تعد الدالة ()print دالةً مرنة، لأننا نستطيع تمرير أي عدد نريده من السلاسل النصية إليها، مثل ('!print('Hello أو (print('My name is', name، ورغم أننا استخدمنا الصيغة * في الفقرة السابقة عند استدعاء الدوال، سنستخدمها الآن أثناء التصريح عن الدوال. لنلقي نظرةً على المثال التالي، وفيه ننشئ دالةً باسم ()product تستقبل أي عدد من الوسطاء لتوجد جدائها: >>> def product(*args): ... result = 1 ... for num in args: ... result *= num ... return result ... >>> product(3, 3) 9 >>> product(2, 1, 2, 3) 12 ليس المعامل args في الدالة السابقة سوى صف عادي في بايثون يتضمن كافة الوسطاء الموضعية. يمكن من الناحية التقنية، تسمية هذا المعامل بأي اسم نريد، شرط أن يُسبق برمز النجمة *، وقد جرت العادة بتسميته args. تتطلب معرفة التوقيت الأنسب لاستخدام الصيغة * بعض التفكير، فالبديل الآخر بغية إنشاء دالة مرنة هو استخدام معامل وحيد يقبل القائمة مثل قيمة مُمرَّرة، أو أي نمط بيانات تكراري آخر، ليتضمن عددًا متغيرًا من العناصر، وهو المبدأ الذي تستخدمه الدالة ()sum المبنية مُسيقًا في بايثون: >>> sum([2, 1, 2, 3]) 8 تتوقع الدالة ()sum تمرير وسيط واحد تكراري إليها، فتمرير عدة وسطاء يؤدي إلى ظهور استثناء كما يلي: >>> sum(2, 1, 2, 3) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: sum() takes at most 2 arguments (4 given) في حين تقبل الدالتين ()min و ()max المبنيتين مُسبقًا في بايثون، واللتان توجدان القيمة الأصغر والقيمة الأكبر على التوالي من بين مجموعة قيم، تمرير وسيط تكراري وحيد إليها أو عدة وسطاء منفردة على النحو التالي: >>> min([2, 1, 3, 5, 8]) 1 >>> min(2, 1, 3, 5, 8) 1 >>> max([2, 1, 3, 5, 8]) 8 >>> max(2, 1, 3, 5, 8) 8 الشيء المشترك بين الدوال السابقة هو إمكانية تمرير أعداد متغيرة من الوسطاء لكل منها، والسؤال الذي يطرح نفسه هنا هو ما سبب تصميم معاملات كل منها على نحو مختلف؟ ومتى يجب أن نصمم دوالًا تقبل وسيطًا تكراريًا وحيدًا ومتى يجب تصميمها لتقبل وسطاء متعددة منفصلة باستخدام الصيغة *؟ تعتمد آلية تصميم المعاملات على الطريقة المتوقعة لاستخدام المبرمج للشيفرة، إذ تقبل الدالة ()print عدة وسطاء لأن المبرمجين يمررون غالبًا سلسلةً من السلاسل النصية أو المتغيرات المتضمنة لسلاسل نصية إليها، مثل (print('My name is', name. يُعد تجميع هذه السلاسل النصية ضمن قائمة ما يتطلب تنفيذ عدة خطوات قبل تمريرها إلى الدالة ()print أمرًا غير شائع، وفي حال تمرير قائمة إلى الدالة ()print، ستُطبع كاملةً بأقواسها المعقوفة ومحارف الفاصلة ما بين العناصر، وبالتالي لا يمكن استخدامها لطباعة قيم قائمة منفردةً. أما بالنسبة للدالة ()sum، فما من سبب لاستدعائها مع تمرير وسطاء منفصلة طالما أنه من الممكن في بايثون استخدام عامل الجمع + لهذا الغرض، إذ يمكن ببساطة كتابة شيفرة مثل 8+4+2، وبالتالي ما من ضرورة لإتاحة إمكانية كتابة شيفرة على النحو (2,4,8)sum، ما يفسر سبب وجوب تمرير العدد المتغير من الوسطاء إلى الدالة ()sum على هيئة قائمة. تتيح الدالتان ()min و ()max استخدام كلا الأليتين؛ فإذا مرر المبرمج إلى أي منهما وسيطًا واحدًا، تفترض الدالة بأن هذا الوسيط هو قائمة أو صف من القيم لتعمل على تقييمها؛ أما إذا مرر إليها عدّة وسطاء، فتفترض أن هذه القيم تمثل ما ستقيّمه، فكلا الدالتين قادرتين على التعامل مع القوائم أثناء تنفيذ البرنامج، كما في الاستدعاء (min(allExpenses، كما أن لديهما القدرة على التعامل مع الوسطاء المنفصلة التي يختارها المبرمج أثناء كتابة شيفراته، كما في الاستدعاء (max(0, someNumber. وبالتالي فإن هاتين الدالتين مصممتين للتعامل مع كلا نوعي الوسطاء. توضح الدالة ()myMinFunction التالية الفكرة، والتي تؤدي نفس وظيفة الدالة ()min: def myMinFunction(*args): if len(args) == 1: 1 values = args[0] else: 2 values = args if len(values) == 0: 3 raise ValueError('myMinFunction() args is an empty sequence') 4 for i, value in enumerate(values): if i == 0 or value < smallestValue: smallestValue = value return smallestValue تستخدم الدالة ()myMinFunction الصيغة * لتستقبل أعدادًا مختلفة من الوسطاء على هيئة صف، فإذا احتوى هذا الصف على قيمة وحيدة، نفترض أنها سلسلة من القيم لتُقيّم كما هو مبين في السطر رقم 1 من الشيفرة السابقة، وفيما عدا ذلك، نفترض أن المعامل arg هو صف من القيم لتُقيّم كما هو مبين في السطر رقم 2 من الشيفرة السابقة.ففي كلتا الحالتين ستتضمن المتغيرات المخصصة لاستقبال القيم على سلسلة من القيم لتعمل باقي أجزاء الشيفرة على تقييمها. كما هو الحال في الدالة ()min الفعلية المبنية مسبقًا في بايثون، عملنا على إظهار خطأ قيمة ValueError في حال استدعاء الدالة دون تمرير أي قيمة أو تمرير سلسلة فارغة إليها، وذلك في السطر رقم 3، أما باقي الشيفرة فتمر على القيم المُمرَّرة لتعيد القيمة الأصغر بينها في السطر رقم 4. بغية تبسيط الدالة ()myMinFunction السابقة، جعلناها تتعامل فقط مع القوائم والصفوف من بين القيم التكرارية الممكنة. قد تتساءل عن سبب عدم كتابة الدوال دومًا لتتعامل مع كلا آليتي تمرير أعداد مختلفة من الوسطاء، والجواب هو أنه من الأفضل دومًا جعل الدوال أبسط ما يمكن، فما لم تكن كلتا طريقتي الاستدعاء شائعة الاستخدام، من الأفضل اختيار إحداهما. إذا كانت الدالة تتعامل عادةً مع بنى معطيات تُنشأ أثناء تنفيذ البرنامج، فمن الأفضل تصميمها لتستقبل معاملًا واحدًا، أما إذا كانت تتعامل عادةً مع وسطاء تُحدد من قبل المبرمج أثناء كتابته للشيفرة، فمن الأفضل في هذه الحالة استخدام الصيغة * لاستقبال أعداد مختلفة من المتغيرات. استخدام ** لإنشاء دوال مرنة يمكن للدوال المرنة أن تستخدم الصيغة ** أيضًا، فرغم كون الصيغة * ضمن تعليمة def تدل على عدد متغير من الوسطاء الموضعيين، إلا أن الصيغة ** تدل على عدد متغير من الوسطاء المسماة الاختيارية؛ فلو صرحنا عن دالة تأخذ عددّا من الوسطاء المسماة الاختيارية دون استخدام الصيغة **، فقد يغدو جزء التصريح هذا صعبًا وغير عملي. لنأخذ دالة مفترضة باسم ()formMolecule لتشكيل مركب كيميائي والتي تمتلك معامل لكل عنصر من العناصر الكيميائية المعروفة البالغ عددها 118: >>> def formMolecule(hydrogen, helium, lithium, beryllium, boron, --snip-- سيكون تمرير القيمة 2 لمعامل عدد ذرات الهيدروجين hydrogen والقيمة 1 لمعامل عدد ذرات الأكسجين oxygen للحصول على جزيء الماء water عمليةً شاقةً وصعبة القراءة، إذ أننا سنضطر إلى إسناد القيمة 0 إلى كل معاملات العناصر غير المطلوبة، على النحو التالي: >>> formMolecule(2, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 --snip-- 'water' يمكن جعل الدالة أكثر تنظيمًا باستخدام معاملات مسماة مع وسيط افتراضي لكل منها، ما يجنبنا عبء تمرير وسيط إلى كل معامل عند استدعاء الدالة. ملاحظة: رغم التعريف الواضح لكلا مصطلحي الوسيط والمعامل، إلا أن المبرمجين عادةً ما يستخدمون مصطلحي الوسيط المسمى keyword argument والمعامل المسمى keyword parameter تبادليًا. على سبيل المثال، عينا في تعليمة التصريح عن الدالة التالية وسيطًا افتراضيًا يساوي القيمة 0 لكل من المعاملات المسماة، على النحو التالي: >>> def formMolecule(hydrogen=0, helium=0, lithium=0, beryllium=0, --snip-- وهذا ما يجعل من استدعاء الدالة ()formMolecule أسهل، لأننا لن نضطر إلى تحديد وسطاء سوى للمعاملات ذات القيم المختلفة عن قيمة الوسيط الافتراضي، كما من الممكن أيضًا تحديد الوسطاء المسماة بأي ترتيب، كما في المثال التالي: >>> formMolecule(hydrogen=2, oxygen=1) 'water' >>> formMolecule(oxygen=1, hydrogen=2) 'water' >>> formMolecule(carbon=8, hydrogen=10, nitrogen=4, oxygen=2) 'caffeine' مع ذلك، تبقى عبارة التصريح السابقة صعبة وغير عملية بوجود 118 اسم معامل، وماذا لو جرى اكتشاف عناصر كيميائية جديدة؟ سنضطر إلى تحديث تعليمة def مع كامل توثيقات معاملات الدالة. يمكن الاعتماد على حل بديل من خلال تجميع كافة المعاملات مع وسطائها على هيئة أزواج مفتاح-قيمة ضمن قاموس، وذلك باستخدام الصيغة ** للوسطاء المسماة، ويمكن -من الناحية التقنية- تسمية المعامل المسبوق بالصيغة ** بأي اسم، إلا أن العادة جرت على تسميته kwargs، على النحو التالي: >>> def formMolecules(**kwargs): ... if len(kwargs) == 2 and kwargs['hydrogen'] == 2 and kwargs['oxygen'] == 1: ... return 'water' ... # (هنا مكان بقية شيفرات الدالة) ... >>> formMolecules(hydrogen=2, oxygen=1) 'water' تشير الصيغة ** إلى أن المعامل kwargs قادر على التعامل مع كافة الوسطاء المسماة الممررة عند استدعاء الدالة، إذ ستُخزن على هيئة أزواج مفتاح-قيمة ضمن قاموس مخصص للمعامل kwargs. الآن، في حال اكتشاف عناصر كيميائية جديدة، كل ما علينا فعله هو تحديث شيرة الدالة وليس عبارة التصريح عنها، ذلك لأن كافة الوسطاء المسماة موضوعة ضمن المعامل kwarg: 1 >>> def formMolecules(**kwargs): 2 ... if len(kwargs) == 1 and kwargs.get('unobtanium') == 12: ... return 'aether' ... # (هنا مكان بقية شيفرات الدالة) ... >>> formMolecules(unobtanium=12) 'aether' نلاحظ من السطر رقم 1 في الشيفرة السابقة أن تعليمة def بقيت كما في الحالة السابقة، وأننا لم نضطر إلى تعديل سوى شيفرة الدالة فقط المشار إليها بالسطر رقم 2، فمع استخدام الصيغة **، تغدو كتابة كل من تعليمة التصريح عن الدالة واستدعائها أبسط مع الحفاظ على سهولة قراءة الشيفرة وفهمها. استخدام * و ** لإنشاء دوال مغلفة Wrapper Functions لعل إحدى الاستخدامات الشائعة لصيغتي * و ** ضمن تعليمة def هي إنشاء الدوال المغلّفة، والتي تُمرر الوسطاء إلى دالة أخرى لتعيد القيمة المعادة الخاصة بتلك الدالة، إذ يمكن استخدام صيغتي * و ** لتمرير أي من وسطاء دالة ما أو جميعها إلى الدالة المغلّفة. على سبيل المثال، يمكن إنشاء دالة باسم ()printLowercase لتكون مغلّفة لدالة ()print المبنية مسبقًا، إذ أنها تعتمد على الدالة ()print في أداء مهمتها الحقيقية في الطباعة، إلا أنها قبل ذلك تحوّل أحرف وسطاء السلاسل النصية إلى حالة الأحرف الصغيرة، على النحو التالي: 1 >>> def printLower(*args, **kwargs): 2 ... args = list(args) ... for i, value in enumerate(args): ... args[i] = str(value).lower() 3 ... return print(*args, **kwargs) ... >>> name = 'Albert' >>> printLower('Hello,', name) hello, albert >>> printLower('DOG', 'CAT', 'MOOSE', sep=', ') dog, cat, moose تستخدم الدالة ()printLower في السطر رقم 1 من الشيفرة السابقة الصيغة * لتتعامل مع أعداد مختلفة من الوسطاء الموضعيين الموجودة ضمن صف مُسند إلى المعامل args، في حين أن الصيغة ** تعمل على إسناد أي وسطاء مسماة إلى قاموس ضمن المعامل kwargs. إذا استخدمت دالة ما كلا المعاملين args* و kwargs** معًا، فيجب أن يأتي المعامل args* قبل المعامل kwargs**، إذ نمرر كلا المعاملين السابقين إلى الدالة المغلّفة ()print، ولكن قبل ذلك تُعدّل دالتنا الجديدة بعضًا من الوسطاء لتجعل الصف الموجود في المعامل args على هيئة قائمة وذلك في السطر المشار إليه بالرقم 2 من الشيفرة أعلاه. نمرّر -بعد تغيير السلاسل النصية الموجودة في المعامل args إلى حالة الأحرف الصغيرة- كلًا من العناصر الموجودة في هذا المعامل إضافةً إلى أزواج مفتاح-قيمة الموجودة في المعامل kwargs مثل وسطاء منفصلين إلى الدالة ()print باستخدام صيغتي * و ** وذلك في السطر المشار إليه بالرقم 3، كما تُعاد القيمة المعادة من الدالة ()print على أنها قيمة الدالة ()printLower المعادة، وهذه الخطوات كفيلة بتغليف الدالة ()print. الخلاصة تعد الدوال طريقةً شائعةً لتجميع شيفرات البرنامج معًا ضمن مجموعات، الأمر الذي يتطلب اتخاذ بعضٍ من القرارات، مثل اختيار اسم الدالة وحجمها وعدد معاملاتها وعدد الوسطاء التي سنمررها إلى هذه المعاملات. وباستخدام الصيغتين * و ** ضمن تعليمة التصريح عن الدوال def نتمكن من جعل الدالة تستقبل أعدادًا مختلفة من المعاملات، ما يجعلها دوالًا مرنة. ترجمة -وبتصرف- للجزء الأول من الفصل العاشر "كتابة دوال فعالة في بايثون" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: غرائب بايثون المخفية كيفية تعريف الدوال في بايثون تعرف على أهم الدوال المدمجة في لغة بايثون
-
تُعد مجموعات القواعد التي تُعرّف لغات البرمجة معقدةً، وقد تؤدي إلى شيفرات ذات سلوكيات أو نتائج غريبة وغير متوقعة رغم كونها غير خاطئة. سنتعمق في هذا المقال أكثر بغرائب بايثون الغامضة. من غير المحتمل أن تواجه هذه الحالات لدى كتابة الشيفرات الواقعية، إلا أنها تمثّل استخدامات مثيرة للاهتمام لصياغة بايثون (أو مثل إساءات لاستخدام هذه الصياغة، حسب منظورك للأمر). ستتعرف من خلال دراستك للأمثلة الواردة في هذا المقال على كيفية عمل بايثون في الكواليس بصورة أوضح، فهيا بنا لنستمتع باستكشاف بعضٍ من الصيغ الصحيحة النادرة والمؤدية إلى وقوع أخطاء esoteric gotchas. لماذا 256 هو 256 ولكن 257 ليس 257؟ يقارن العامل == بين كائنين ليتحقق من تساوي قيمتيهما values، في حين يقارن العامل is بينهما للتحقق من تساوي هويتهما IDs، فبالرغم من أن للعدد الصحيح 42 (من النوع integer) والعدد الحقيقي 42.0 (من النوع float) القيمة نفسها، إلا أنهما يمثلان كائنان مختلفان محفوظان في مكانين مختلفين من ذاكرة الحاسوب، ويمكن التأكد من الأمر بالتحقق من هوياتهما المختلفة باستخدام الدالة ()id: >>> a = 42 >>> b = 42.0 >>> a == b True >>> a is b False >>> id(a), id(b) (140718571382896, 2526629638888) فلدى إنشاء بايثون لكائن عدد صحيح جديد وتخزينه في الذاكرة، يستغرق إنشاء هذا الكائن فترةً زمنيةً قصيرة جدًا. يُنشئ المفسر CPython (مُفسّر بايثون المتوفر للتحميل عبر الرابط) كائنات أعداد صحيحة للأعداد من 5- وحتى 256 في بداية كل برنامج مثل نوع من التحسين البسيط، وتدعى هذه الأعداد بالأعداد الصحيحة المُخصصة مُسبقًا، ويُنشئ مفسر بايثون كائنات لهذه الأعداد تلقائيًا نظرًا لكونها شائعة الاستخدام، فمن الشائع أن يُستخدم العدد 0 أو 2 أكثر من العدد 1729 مثلًا، وعند إنشاء كائن عدد صحيح جديد في الذاكرة، يتحقق CPython بدايةً من كونه غير محصور بين 5- و256؛ فإذا كان فعلًا كذلك، يوفر CPython الوقت باستعادة كائن العدد الصحيح المُنشأ أصلًا والموجود بدلًا من إنشاء كائن جديد. توفر هذه الطريقة أيضًا الذاكرة بعدم تخزين نسخ متطابقة لأعداد صحيحة صغيرة، كما هو موضح في الشكل 9-1. الشكل 9-1: توفّر بايثون الذاكرة باستخدام مراجع متعددة لنفس كائن العدد الصحيح (على اليسار)، بدلًا من كائنات أعداد صحيحة مكررة منفصلة من أجل كل مرجع (على اليمين). دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن وبسبب التحسين آنف الذكر، فمن الممكن أن تؤدي بعض الحالات المفتعلة إلى نتائج غريبة، وكمثال على ذلك، لنكتب ما يلي في الشيفرة التفاعلية: >>> a = 256 >>> b = 256 1 >>> a is b True >>> c = 257 >>> d = 257 2 >>> c is d False جميع كائنات العدد 256 هي في الواقع نفس الكائن (المُنشأ تلقائيًا من قبل مفسر بايثون)، وبالتالي فإن العامل is للمتغيرين a و b سيعيد القيمة True دلالةً على تطابق هويتاهما كما في السطر رقم 1، في حين تُنشئ بايثون كائنات مستقلة للعدد 257 في المتغيرين c و d، ما يفسر إعادة العامل is للقيمة False في السطر رقم 2 من الشيفرة السابقة. أما التعبير البرمجي 257 is 257 فيُقيّم إلى True، إذ تستخدم بايثون كائن العدد الصحيح نفسه المُنشأ للقيم المجردة المتطابقة ضمن التعليمة الواحدة، كما في المثال التالي: >>> 257 is 257 True ومما لا شك فيه أن البرامج الواقعية تستخدم عادةً قيمة العدد الصحيح وليس هويته، وبالتالي لن نستخدم العامل is لمقارنة الأعداد الصحيحة integers أو العشرية floats أو السلاسل النصية strings أو القيم المنطقية bools أو أي قيم من أنماط البيانات البسيطة الأخرى، ما عدا استثناء وهو حالة استخدام التعبير is None بدلًا من None ==، وفيما عدا ذلك فمن النادر الوقوع في هذا الخطأ. التخزين المشترك للسلاسل النصية المتطابقة String Interning تستخدم بايثون -على نحوٍ مشابه لما سبق- نفس الكائنات للتعبير عن السلاسل النصية المجردة المتطابقة في الشيفرة بدلًا من إنشاء نسخ مستقلة عن نفس السلسلة. لنكتب مثلًا الشيفرة التالية في الصدفة التفاعلية: >>> spam = 'cat' >>> eggs = 'cat' >>> spam is eggs True >>> id(spam), id(eggs) (1285806577904, 1285806577904) لاحظت بايثون في الشيفرة السابقة أن السلسلة النصية المجردة 'cat' المسندة للمتغير eggs هي نفس السلسلة 'cat' المسندة إلى المتغير spam، لذا وبدلًا من إنشاء كائن ثاني مُكرَّر لنفس السلسلة، تُسند بايثون للمتغير eggs مرجعًا إلى نفس كائن السلسلة المُستخدم من قبل المتغير spam، وهذا ما يفسر كون هويات السلاسل النصية لكلا المتغيرين متساوية. يدعى هذا التحسين باسم التخزين المشترك للسلاسل النصية المتطابقة String Interning، وبما يشبه مبدأ التخصيص المُسبق للأعداد الصحيحة، فهذا التحسين ليس أكثر من تفصيل مُقدّم من المُفسر CPython، وبالتالي يجب عدم الاعتماد عليه تمامًا، ناهيك عن كون هذا هذا التحسين غير قادر على اكتشاف جميع السلاسل النصية المتطابقة الممكنة. فالبحث عن كل نسخة من الممكن تطبيق هذا التحسين عليها قد يستغرق وقتًا أطول من ذلك الذي يوفره هذا التحسين أصلًا. فعلى سبيل المثال، لدى محاولة إنشاء السلسلة النصية cat انطلاقًا من السلسلتين c و at في الصدفة التفاعلية، نلاحظ أن CPython قد أنشأ السلسلة النهائية cat ككائن سلسلة نصية جديد بدلًا من إعادة استخدام كائن السلسلة النصية الخاص بالمتغير spam، على النحو التالي: >>> fish = 'c' >>> fish += 'at' >>> spam is fish False >>> id(spam), id(fish) (1285806577904, 1285808207384) التخزين المشترك للسلاسل النصية المتطابقة هو تقنية تحسين تستخدمها المفسرات والمترجمات في العديد من لغات البرمجة. يمكنك الاطلاع على مزيدٍ من التفاصيل من خلال الرابط. عوامل الزيادة والإنقاص الزائفة في بايثون يمكنك زيادة قيمة متغير ما في بايثون أو إنقاصها بمقدار 1 باستخدام عوامل الإسناد المعززة augmented؛ فالشيفرة spam += 1 و spam -= 1 تزيد القيمة العددية في المتغير spam وتنقصها بمقدار 1 على التوالي. في حين أن لغات برمجة أخرى مثل لغة ++C ولغة جافا سكريبت تمتلكان العاملين ++ و -- لعمليات الزيادة والإنقاص. يدل الاسم "++C" بحد ذاته عن ذلك، فهي نكتة من قبيل المبالغة الساخرة تشير لكون "++C" إصدار مُحسّن من لغة C، إذ قد تتضمن الشيفرات بلغة ++C أو جافا سكريبت عمليات مثل spam++ أو ++spam، في حين أن بايثون قد اتخذت قرارها الحكيم بعدم تضمين هذين العاملين كونهما مصدر شهير للأخطاء الدقيقة. ولكن من المسموح تمامًا في بايثون كتابة الشيفرة التالية: >>> spam = --spam >>> spam 42 أول تفصيل ينبغي ملاحظته هو أن أي من العاملين ++ و -- في بايثون لا يَزيد أو يُنقص القيمة في المتغير spam، إذ تدل إشارة - الاستهلالية على عامل النفي الأحادي في بايثون، بمعنى أنه من المسموح كتابة شيفرة بالشكل التالي: >>> spam = 42 >>> -spam -42 وبالتالي من المسموح استخدام عدة عوامل نفي أحادي قبل قيمة ما. سنحصل مع استخدام اثنين منها على القيمة السالبة للقيمة السالبة للقيمة الأساسية، والتي تمثّل القيمة الأصلية نفسها لحالة القيم العددية الصحيحة، على النحو التالي: >>> spam = 42 >>> -(-spam) 42 لعله من السُخف استخدام عملية كتلك أعلاه، ولن تصادف غالبًا عامل نفي أحادي مُستخدم مرتين متتاليتين في الشيفرات الواقعية، وإذا صادفته، فأنت تقرأ غالبًا شيفرةً مكتوبةً من قبل مبرمج قد تعلّم لغة برمجة أخرى وقد كتب شيفرة بايثون على نحوٍ خاطئ. يوجد عامل + أحادي، وهو يقيّم قيمة العدد الصحيح إلى نفس إشارته كما هي في قيمته الأصلية، وبالتالي فهو حرفيًا لا يؤدي أي غرض: >>> spam = 42 >>> +spam 42 >>> spam = -42 >>> +spam -42 تبدو كتابة 42+، أو 42++ بنفس سخافة كتابة 42- -'، فما هدف بايثون من تضمين هذان العاملان الأحاديان؟ إنها موجودة فقط لإتمام العامل-` في حال الحاجة إلى زيادة تحميل هذه العوامل في أصنافك الخاصة. قد تكون المصطلحات في الجملة السابقة غير مألوفة بالنسبة لك، ولكن ستتعرف على مفهوم زيادة تحميل العامل operator overloading في مقال لاحق. ويصلح استخدام كل من العاملين + و - فقط قبل القيم في بايثون وليس بعدها؛ ففي حين أن تعابير مثل --spam و ++spam مسموحة في ++C أو جافا سكريبت، إلا أنها تسبب أخطاء صياغة في بايثون: >>> spam++ File "<stdin>", line 1 spam++ ^ SyntaxError: invalid syntax إذًا، لا تحتوي بايثون على عوامل زيادة وإنقاص، رغم أن صياغة بايثون توحي بامتلاكها لهذه العوامل. الكل من اللاشيء تستقبل دالة ()all المبنية مُسبقًا في بايثون قيمةً متسلسلةً sequence value مثل القائمة، لتعيد True في حال كون جميع القيم في المتسلسلة تحقق معيارًا ما أو ليست خاطئة منطقيًا "Truthly"، وتعيد القيمة False في حال كون إحدى قيم المتسلسلة أو أكثر لا تحقق ذلك المعيار أو أنها خاطئة منطقيًا "Falsey". يمكن استخدام الدالة ()all جنبًا إلى جنب مع بناء اشتمال القوائم list comprehensions لإنشاء قائمة من قيم منطقية بناءً على قائمة أخرى عبر تقييم عناصرها وفق معيار معين. لنكتب مثلًا ما يلي في الصدفة التفاعلية: >>> spam = [67, 39, 20, 55, 13, 45, 44] >>> [i > 42 for i in spam] [True, False, False, True, False, True, True] >>> all([i > 42 for i in spam]) False >>> eggs = [43, 44, 45, 46] >>> all([i > 42 for i in eggs]) True تعيد الدالة ()all القيمة True في حال كون كافة الأعداد في القائمة spam أو eggs أكبر من العدد 42، ولكن ماذا لو مررنا متسلسلة فارغة إلى الدالة ()all، ستعيد دومًا في هذه الحالة القيمة True. لنكتب الشيفرة التالية في الصدفة التفاعلية: >>> all([]) True يُفضّل فهم التعبير ([])all على أنه تقييم للإدعاء القائل: "ليس أي من العناصر في القائمة خاطئ منطقيًا" بدلًا من الإدعاء: "جميع العناصر في القائمة صحيحة منطقيًا"، وعدا ذلك ستحصل على نتائج غريبة لا تتوقعها، فعلى سبيل المثال، لنكتب التالي في الصدفة التفاعلية: >>> spam = [] >>> all([i > 42 for i in spam]) True >>> all([i < 42 for i in spam]) True >>> all([i == 42 for i in spam]) True تبدو الشيفرة السابقة وكأنها تُظهر أن جميع القيم في القائمة spam (والتي هي قائمة فارغة) أكبر من العدد 42، وبأنها بنفس الوقت أصغر من العدد 42 وأنها أيضًا تساوي العدد 42، الأمر المستحيل من الناحية المنطقية، ولكن مع ملاحظة أن كلًا من بنى اشتمال القوائم هذه تُقيّم على أنها قائمة فارغة، يتضح سبب عدم وجود أي عنصر في كل منها ليُعد خاطئ منطقيًا وبالتالي يتضح سبب إعادة الدالة ()all للقيمة True في كل مرة. القيم المنطقية هي في الواقع أعداد صحيحة تمامًا كما تعد بايثون القيمة الحقيقية "42.0" (من النوع العشري float) على أنها تساوي القيمة الصحيحة "42" (من نوع العدد الصحيح integer)، فإنها تعد القيم المنطقية True و False على أنها تكافئ 1 و 0 على التوالي؛ إذ يُعد [نمط بيانات القيم المنطقية bool صنفًا فرعيًا من نمط البيانات int في بايثون، ويمكن استخدام الدالة ()int لتحويل القيم المنطقية إلى أعداد صحيحة على النحو التالي: >>> int(False) 0 >>> int(True) 1 >>> True == 1 True >>> False == 0 True كما يمكن استخدام الدالة ()isinstance للتأكد من عدّ قيمة منطقية على أنها من نمط الأعداد الصحيحة، كما يلي: >>> isinstance(True, bool) True >>> isinstance(True, int) True القيمة True هي من نمط البيانات المنطقية، لكن ونظرًا لكون النمط bool هو صنف فرعي من صنف الأعداد الصحيحة int، تُعد القيمة True أيضًا من النمط int، ما يعني أنه يمكن استخدام كل من القيمتين True و False تقريبًا في كل موضع يمكن استخدام الأعداد الصحيحة فيه، ما قد يعطي شيفرة غريبة إلى حدٍ ما: >>> True + False + True + True # Same as 1 + 0 + 1 + 1 3 >>> -True # Same as -1. -1 >>> 42 * True # Same as 42 * 1 mathematical multiplication. 42 >>> 'hello' * False # Same as 'hello' * 0 string replication. ' ' >>> 'hello'[False] # Same as 'hello'[0] 'h' >>> 'hello'[True] # Same as 'hello'[1] 'e' >>> 'hello'[-True] # Same as 'hello'[-1] 'o' ومما لاشك فيه أن حقيقة إمكانية استخدام القيم المنطقية مثل أعداد لا يعني وجوب ذلك، فالشيفرات السابقة غير مقروءة ويجب عدم استخدامها إطلاقًا في الشيفرات الواقعية، وبايثون بالأساس لم تكن تتضمن نمط بيانات القيم المنطقية bool، إذ لم تُضاف القيم المنطقية إليها حتى الإصدار 2.3، وفيه أُنشأ الصنف الفرعي bool من الصنف int لتسهيل تنفيذه. تعدّ True و False كلمات مفتاحية محجوزة بدءًا من الإصدار 3 من بايثون، ما يعني أنه في الإصدار 2 من بايثون كان من الممكن استخدام True و False مثل أسماء للمتغيرات، ما يؤدي إلى شيفرة توحي بالتناقض كما يلي: Python 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:25:58) [MSC v.1500 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. >>> True is False False >>> True = False >>> True is False True لحسن الحظ، لم يعد هذا النوع من الشيفرات الغريبة ممكنًا في الإصدار 3 من بايثون، إذ ستتسبب شيفرة مثل تلك الموجودة في المثال أعلاه بخطأ صياغة، بسبب استخدام إحدى الكلمتين المفتاحيتين (المحجوزتين) True أو False اسمًا لمتغير. استخدام سلسلة من العوامل المختلفة قد يؤدي استخدام سلسلة من العوامل مختلفة الأنواع ضمن تعبير برمجي واحد إلى ظهور أخطاء غير متوقعة، فعلى سبيل المثال، في الشيفرة (غير الواقعية) التالية نستخدم كلا العاملين == و in ضمن تعبير برمجي واحد، على النحو التالي: >>> False == False in [False] True تثير النتيجة السابقة المتمثلة بالقيمة True الاستغراب، إذ أننا نتوقع تقييمها بإحدى الطريقتين: [False == False) in [False) والذي يُقيّم إلى False. ([False == (False in [False والذي يُقيّم إلى False أيضًا. إلا أن التعبير البرمجي [False == False in [False لا يكافئ أي من التعبيرين السابقين، إذ أنه يكافئ التعبير التالي: (False == False) and (False in [False]) تمامًا كما التعبير البرمجي: 42 < spam < 99 يكافئ التعبير: (42 < spam) and (spam < 99) فبالنتيجة يًقيّم التعبير في المثال السابق كما في المخطط التالي: يمثّل التعبير البرمجي [False == False in [False أحجية مسلية في بايثون، ولكن من غير المحتمل أن تصادفك في الشيفرات الواقعية. ميزة مقاومة الجاذبية Antigravity في بايثون لتفعيل ميزة مقاومة الجاذبية في بايثون، نكتب ما يلي في الصدفة التفاعلية: >>> import antigravity يمثّل هذا السطر البرمجي حيلةً مخفية تعمل على فتح متصفح الويب على كاريكاتير فكاهي تقليدي حول بايثون من المدونة الكوميدية XKCD على الرابط، وقد تستغرب من قدرة بايثون على فتح متصفح الويب، إلا أن هذه ميزة مبنية مُسبقًا توفرها وحدة متصفح الويب webbrowser، والتي تحتوي على الدالة ()open، وتبحث هذه الدالة عن متصفح الويب الافتراضي في نظام التشغيل لديك، لتفتح نافذةً فيه على عنوان URL معيّن. دعنا نكتب التالي في الصدفة التفاعلية كمثال: >>> import webbrowser >>> webbrowser.open('https://xkcd.com/353/') رغم محدودية الوحدة webbrowser، إلا أنها مفيدة في حال الرغبة بتوجيه المستخدم إلى صفحة ويب ما للحصول على مزيد من المعلومات حول موضوع ما. الخلاصة من السهل نسيان أن من صمم الحواسيب وأيضًا لغات البرمجة هم بشر أي غير معصومين عن الخطأ فالكثير من البرمجيات مبنية اعتمادًا على ابتكارات مصممي اللغات ومهندسي التجهيزات الصلبة، فهم يعملون ما بوسعهم للتأكد من أن أي خطأ يصادفك سيكون ناجمًا عن خطأ في برنامجك نفسه، وليس بسبب برنامج المفسّر أو تجهيزة وحدة المعالجة المركزية العامل عليها، ما يجعلنا نتعامل مع صحة عمل هذه الأدوات مثل مسلمات. تأتي من هنا قيمة تعلّم الدهاليز والغرائب في الحاسوب والبرامج، ففي حال تسببت شيفرتك بظهور خطأ أو توقفت عن العمل أو حتى أعطت نتائج غريبة غير متوقعة، عليك أن تكون على دراية بالصيغ الشائعة الصحيحة المؤدية إلى الأخطاء gotchas في سبيل تنقيح هذه الأخطاء. لن تصادفك غالبًا أي من المشاكل التي استعرضنا في هذا المقال، إلا أن معرفتك لهذه التفاصيل الصغيرة من شأنه أن يجعلك مبرمج بايثون خبير. ترجمة -وبتصرف- للفصل التاسع "غرائب بايثون المخفية" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: البنى الصحيحة المؤدية إلى الأخطاء الشائعة في بايثون كتابة شيفرات بايثون: صيغ شائعة الاستخدام على نحو خاطئ اكتشاف دلالات الأخطاء في شيفرات لغة بايثون ثلاثة أخطاء عليك تفاديها عند تعلم البرمجة بلغة بايثون
-
رغم كون بايثون لغة البرمجة المفضلة لدى العديد من المبرمجين، إلا أنها لا تخلو من بعض العيوب؛ والتي تتفاوت بين لغة وأخرى، وهنا لا تعد بايثون استثناء، فعلى مبرمجي بايثون الجدد تعلم كيفية تجنّب بعض من البنى الشائعة التي تبدو صحيحة إلا أنها تؤدي بالنتيجة إلى حدوث أخطاء والتي ندعوها "gotchas". يكتسب عادةً المبرمجون هذه المعرفة عشوائيًا مع الخبرة، إلا أن هذا المقال يجمّع هذه البنى الشائعة لك في مكانٍ واحد، إذ ستتمكن من خلال معرفتك للتقاليد البرمجية الكائنة خلف هذه البنى من فهم سلوكيات بايثون الغريبة التي ستصادفك أحيانًا. يوضّح هذا المقال أسباب السلوكيات غير المتوقعة للكائنات المتغيرة، مثل القوائم والقواميس، والتي تظهر لدى تغيير محتوياتها، كما ستتعلم أن تابع الفرز ()sort لا يفرز العناصر وفق ترتيب أبجدي تمامًا، وكيف يمكن لأخطاء التقريب أن تجد طريقها نحو الأعداد ذات الفاصلة العشرية، كما أن لعامل عدم التساوي =! سلوك غير طبيعي يظهر لدى استخدام سلسلة منه، ولابد أيضًا من استخدام فاصلة زائدة إضافية عند كتابة صف وحيد العنصر. باختصار: يوجهك هذا المقال إلى كيفية تجنُّب البنى الصحيحة المسببة للأخطاء gotchas. لا تحذف أو تضيف عناصر إلى قائمة أثناء المرور على عناصرها تؤدي غالبًا إضافة عناصر إلى قائمة أو حذفها منها أثناء المرور على عناصرها باستخدام حلقة for أو while إلى حدوث أخطاء. لنأخذ بالحسبان الحالة التالية مثالًا: نرغب بالمرور على قائمة من السلاسل النصية التي تصف عناصر من الملابس بغية التأكد من أن عدد عناصر الجوارب زوجي، وذلك بإدخال عنصر جوارب موافق جديد لكل مرة يوجد فيها عنصر جوارب في القائمة، وهنا تبدو المهمة بسيطة وواضحة؛ إذ أنها تتمثل بالمرور على السلاسل النصية في القائمة وعندما نجد عنصر جوارب 'sock' في إحدى السلاسل النصية مثل 'red sock' فإننا نُدخل سلسلة 'red sock' أخرى جديدة إلى القائمة. لن تعمل شيفرة كهذه، لأنها ستدور في حلقة لا نهائية، ولن نستطيع إيقافها ما لم نضغط على مفتاحي "CTRL-C" لإيقافها قسريًا، على النحو التالي: >>> clothes = ['skirt', 'red sock'] >>> for clothing in clothes: # Iterate over the list. ... if 'sock' in clothing: # Find strings with 'sock'. ... clothes.append(clothing) # Add the sock's pair. ... print('Added a sock:', clothing) # Inform the user. ... Added a sock: red sock Added a sock: red sock Added a sock: red sock --snip-- Added a sock: red sock Traceback (most recent call last): File "<stdin>", line 3, in <module> KeyboardInterrupt وبإمكانك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة بزيارة الرابط. تكمن المشكلة في أنه عند إضافة العنصر 'red sock' إلى القائمة clothes، فسيصبح لدى هذه القائمة عنصر ثالث جديد، والذي يجب المرور عليه، وهو: ['skirt', 'red sock', 'red sock'] وبالتالي ستصل حلقة for إلى العنصر 'red sock' الثاني الجديد عند التكرار التالي مُضيفةً سلسلة 'red sock' أخرى إلى القائمة لتصبح على النحو: ['skirt', 'red sock', 'red sock', 'red sock'] مضيفةً سلسلةً نصيةً جديدةً إلى القائمة لتمر عليها بايثون. يستمر هذا الأمر بالحدوث كما هو موضح في الشكل 8-1 التالي، وهو سبب ظهور التدفق اللامتناهي من رسائل إضافة عنصر جوارب جديد 'Added a sock' إلى القائمة، وستستمر الحلقة بالتكرار حتى امتلاء ذاكرة الحاسب وبالتالي توقف برنامج بايثون عن العمل، أو حتى نضغط على مفتاحي "CTRL-C" في لوحة المفاتيح. الشكل 8-1: يُضاف عنصر 'red sock' جديد إلى القائمة من أجل كل تكرار للحلقة for، إذ تشير قيمة المتغير clothing إلى قيمة التكرار التالي، وتتكرر هذه الحلقة لانهائيًا. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن يكون الحل بعدم إضافة عناصر إلى القائمة الحالية أثناء المرور عليها، وبالتالي سنستخدم قائمة منفصلة لمحتويات القائمة الجديدة المعدلة مثل القائمة newClothes في المثال التالي لتكون بمثابة حل بديل: >>> clothes = ['skirt', 'red sock', 'blue sock'] >>> newClothes = [] >>> for clothing in clothes: ... if 'sock' in clothing: ... print('Appending:', clothing) ... newClothes.append(clothing) # We change the newClothes list, not clothes. ... Appending: red sock Appending: blue sock >>> print(newClothes) ['red sock', 'blue sock'] >>> clothes.extend(newClothes) # Appends the items in newClothes to clothes. >>> print(clothes) ['skirt', 'red sock', 'blue sock', 'red sock', 'blue sock'] بإمكانك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة بزيارة الرابط. تمر الحلقة for في الشيفرة السابقة على عناصر القائمة clothes دون تعديلها، إذ تجري التعديلات ضمن قائمة مستقلة باسم newClothes، وبعد الانتهاء من المرور على عناصر القائمة clothes، نعدّلها بإضافة محتويات القائمة newClothes إليها، وبذلك نحصل على قائمة clothes تحتوي على عدد زوجي من أزواج الجوارب المتوافقة. على نحوٍ مشابه، لا ينبغي حذف عناصر من قائمة أثناء المرور على عناصرها، مثل شيفرة تحذف أي سلسلة نصية ليست hello من قائمة ما. تتمثل المنهجية الأكثر سذاجة في هذه الحالة بالمرور على عناصر القائمة وحذف العناصر غير المساوية للقيمة hello، على النحو التالي: >>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello'] >>> for i, word in enumerate(greetings): ... if word != 'hello': # Remove everything that isn't 'hello'. ... del greetings[i] ... >>> print(greetings) ['hello', 'hello', 'yello', 'hello'] نلاحظ في نتيجة تنفيذ الشيفرة السابقة أن العنصر 'yello' بقي موجودًا في القائمة الناتجة، والسبب وراء ذلك هو أنه لدى وصول تكرار الحلقة for إلى العنصر ذو الفهرس (المؤشر) رقم 2، ستحذف العنصر المقابل وهو 'mello' من القائمة، ما سيؤدي إلى إزاحة فهرس كل من العناصر المتبقية بمقدار واحد نزولًا، وبالتالي يصبح فهرس العنصر 'yello' هو 2 بدلًا من 3. والتكرار التالي للحلقة سيختبر العنصر ذو الفهرس رقم 3، والذي أصبح الآن السلسلة النصية 'hello' الأخيرة، كما هو موضح في الشكل 8-2. إذ نلاحظ أن السلسلة النصية 'yello' لم تُختبر أصلًا، وبالتالي نؤكد مجددًا على ضرورة عدم حذف عناصر من قائمة أثناء المرور على عناصرها. الشكل 8-2: بعد مرور الحلقة على العنصر 'mello' واختباره وحذفه، تُزاح فهارس العناصر المتبقية نزولًا بمقدار واحد، ما يجعل المؤشر i يتجاوز العنصر 'yello'. يمكن بدلًا من ذلك إنشاء قائمة جديدة لننسخ فيها كافّة العناصر من القائمة الأصلية عدا تلك التي نريد حذفها، لنستبدل القائمة الأصلية بها كنتيجة. سنكتب الشيفرة التالية في الصدفة التفاعلية لتكون مكافئةً للمثال السابق لكن دون أخطاء: >>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello'] >>> newGreetings = [] >>> for word in greetings: ... if word == 'hello': # Copy everything that is 'hello'. ... newGreetings.append(word) ... >>> greetings = newGreetings # Replace the original list. >>> print(greetings) ['hello', 'hello', 'hello'] يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة بزيارة الرابط. تجدر الملاحظة أنه وبما أن الشيفرة السابقة مجرد حلقة بسيطة تُنشِئ قائمة، فمن الممكن استبدالها ببنية اشتمال قوائم list comprehensions وحدها. لن تعمل هذه القوائم أسرع ولن تستخدم مساحةً أقل من الذاكرة، إلا أنها أسهل للكتابة وتجعل من مقروئية الشيفرة أفضل. لنكتب الشيفرة التالية ضمن الصدفة التفاعلية لتكون مكافئة للشيفرة السابقة: >>> greetings = ['hello', 'hello', 'mello', 'yello', 'hello'] >>> greetings = [word for word in greetings if word == 'hello'] >>> print(greetings) ['hello', 'hello', 'hello'] لا يُعد استخدام بنية اشتمال قوائم أكثر إيجازًا فحسب، بل ويجنبنا وقوع أخطاء من شيفرات تبدو صحيحة gotcha، والتي تحدث لدى تغيير عناصر قائمة أثناء المرور عليها. المراجع واستخدام الذاكرة والدالة ()sys.getsizeof قد يبدو إنشاء قائمة جديدة بدلًا من تعديل الأصلية نفسها هدرًا للذاكرة، لكن علينا تذكر أنه كما أن المتغيرات تحتوي من الناحية التقنية على مراجع للقيم وليس على القيم نفسها، كذلك الأمر بالنسبة للقوائم، إذ تحتوي على مراجع للقيم؛ فالسطر البرمجي (newGreetings.append(word المُستخدَم سابقًا لا يُنشئ نسخةً عن السلسلة النصية الموجودة في المتغير word، وإنما ينسخ فقط مرجع هذه السلسلة النصية وهو أصغر بكثير منها حجمًا. يمكن التأكد من ذلك باستخدام الدالة ()sys.getsizeof التي تعيد عدد البايتات التي يحجزها الكائن في الذاكرة. نلاحظ في مثالنا التالي في الصدفة التفاعلية أن السلسلة النصية القصيرة 'cat' تحجز 52 بايتًا، في حين أن سلسلة نصية أطول تحجز 85 بايتًا، على النحو التالي: >>> import sys >>> sys.getsizeof('cat') 52 >>> sys.getsizeof('a much longer string than just "cat"') 85 في إصدار بايثون الذي نستخدم، يُحجَز 49 بايتًا لحمل overhead كائن السلسلة النصية، في حين يُحجَز بايت واحد لكل محرف، أما القائمة وبغض النظر عن السلاسل النصية التي تتضمنها فإنها تحجز 72 بايت، مهما كان طول هذه السلاسل النصية، كما يلي: >>> sys.getsizeof(['cat']) 72 >>> sys.getsizeof(['a much longer string than just "cat"']) 72 السبب وراء ذلك هو أن القوائم من الناحية التقنية لا تحتوي على السلاسل النصية نفسها وإنما عناوينها فقط، وللمرجع الحجم نفسه بغض النظر عن البيانات التي يشير إليها؛ فتعليمة مثل (newGreetings.append(word لا تنسخ السلسلة النصية الموجودة في المتغير word وإنما تنسخ مرجعها فقط. إذا كنت ترغب بمعرفة الحجم الذي يحجزه كائن ما مع كل الكائنات التي يشير إليها، يمكنك استخدام الدالة التي أنشأها مطور نواة بايثون "ريمون هيتينجر Raymond Hettinger" لهذا الغرض والمتوفرة على الرابط. ينبغي ألا تعتقد بأنك تهدر ذاكرة بإنشائك لقائمة جديدة بدلًا من التعديل في نفس القائمة أثناء المرور على عناصرها، وحتى وإن بدت شيفرتك تعمل ظاهريًا، فقد تكون هي مصدر لأخطاء دقيقة تتطلب وقتًا طويلًا لاكتشافها وإصلاحها؛ فوقت المبرمج أثمن من الذاكرة، وهدره مكلف أكثر بكثير من هدرها. بالعودة الآن إلى موضوعنا، ورغم أنه لا يجب إضافة أو حذف عناصر من قائمة أو أي كائن تكراري آخر أثناء المرور على عناصرها، لكن من الممكن تعديل عناصر القائمة أثناء ذلك؛ فعلى سبيل المثال، ليكن لدينا قائمة من الأعداد في صورة سلاسل نصية بالشكل: ['5', '4', '3', '2', '1'] من الممكن تحويل قائمة السلاسل النصية هذه إلى قائمة من الأعداد الصحيحة [5, 4, 3, 2, 1] أثناء المرور على عناصرها، على النحو التالي: >>> numbers = ['1', '2', '3', '4', '5'] >>> for i, number in enumerate(numbers): ... numbers[i] = int(number) ... >>> numbers [1, 2, 3, 4, 5] يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة بزيارة الرابط، فتعديل العناصر في القائمة مقبول، لا سيما أنه قد يقلل عدد العناصر المعرضة للأخطاء فيها. إحدى الطرق الأخرى الممكنة لإضافة أو حذف عناصر من قائمة بأمان هي بالمرور على العناصر عكسًا من نهاية القائمة إلى بدايتها، ويمكن بهذه الطريقة حذف عناصر من القائمة أثناء المرور عليها، أو حتى إضافة عناصر جديدة إليها دون أن يكون لها أثرًا طالما أننا نضيفها إلى نهاية القائمة، فعلى سبيل المثال، لنكتب الشيفرة التالية التي تحذف الأعداد الزوجية من القائمة المسماة someInts: >>> someInts = [1, 7, 4, 5] >>> for i in range(len(someInts)): ... ... if someInts[i] % 2 == 0: ... del someInts[i] ... Traceback (most recent call last): File "<stdin>", line 2, in <module> IndexError: list index out of range >>> someInts = [1, 7, 4, 5] >>> for i in range(len(someInts) - 1, -1, -1): ... if someInts[i] % 2 == 0: ... del someInts[i] ... >>> someInts [1, 7, 5] ستعمل الشيفرة السابقة على نحوٍ سليم، لأن فهرس أي من العناصر التي ستمر عليها الحلقة لاحقًا لن يتغير أبدًا، إلا أن الإزاحة المتكررة للقيم الواقعة بعد تلك المحذوفة تجعل من هذه التقنية غير فعّالة لا سيما للقوائم الطويلة. يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة عبر الرابط. يوضح الشكل 8-3 التالي الفرق ما بين المرور المباشر (الأمامي) والعكسي على عناصر القائمة. الشكل 8-3: حذف الأعداد الزوجية من قائمة أثناء المرور على عناصرها عكسيًا (على اليسار) وأماميًا (على اليمين). يمكن بصورةٍ مشابهة للحالة السابقة إضافة عناصر إلى نهاية القائمة أثناء المرور عليها عكسيًا. لنكتب الشيفرة التالية في الصدفة التفاعلية، والتي تضيف نسخةً من أي عدد زوجي تصادفه إلى نهاية القائمة somwInts على النحو التالي: >>> someInts = [1, 7, 4, 5] >>> for i in range(len(someInts) - 1, -1, -1): ... if someInts[i] % 2 == 0: ... someInts.append(someInts[i]) ... >>> someInts [1, 7, 4, 5, 4] بإمكانك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة بزيارة الرابط. إذًا، يمكن بالمرور عكسًا على عناصر القائمة إضافة عناصر إلى القائمة أو حذفها منها، وقد يكون تنفيذ هذه التقنية على النحو الصحيح صعبًا، إذ قد يؤدي أدنى تغيير فيها بالنتيجة إلى وقوع الأخطاء، وبالتالي يبقى من الأسهل والأبسط إنشاء قائمة جديدة بدلًا من التعديل على القائمة الأصلية. وصّف مطور نواة بايثون ريمون هيتينجر: لا تنسخ القيم المتغيرة Mutable Values دون استخدام الدالتين ()copy.copy و ()copy.deepcopy لعله من الأفضل فهم المتغيرات على أنها لافتات أو أسماء وسوم تشير إلى الكائنات بدلًا من فهمها على أنها صناديق تحتوي على الكائنات نفسها، ويؤتي هذا الفهم ثماره لدى تعديل الكائنات المتغيرة، وهي كائنات مثل القوائم والقواميس والمجموعات، والتي قيمها قابلة للتغيير mutable؛ فإذا كان لدينا متغير يشير إلى كائن ما متغيّر، وأردنا نسخ هذا المتغير إلى متغير آخر، فإن اعتقادنا بأن الكائن نفسه سيُنسخ يمثل مصدرًا شائعًا لأخطاء gotcha. إذ أن تعليمات الإسناد في بايثون لا تنسخ الكائنات، وإنما تنسخ فقط المراجع المشيرة إليها. لنكتب مثلًا الشيفرة التالية في الصدفة التفاعلية، ونلاحظ منها أنه رغم تغيير المتغير spam وحده، فإن المتغير cheese قد تغير أيضًا: >>> spam = ['cat', 'dog', 'eel'] >>> cheese = spam >>> spam ['cat', 'dog', 'eel'] >>> cheese ['cat', 'dog', 'eel'] >>> spam[2] = 'MOOSE' >>> spam ['cat', 'dog', 'MOOSE'] >>> cheese ['cat', 'dog', 'MOOSE'] >>> id(cheese), id(spam) 2356896337288, 2356896337288 يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة على الرابط. إذا كنت تعتقد بأن التعليمة cheese=spam تنسخ كائن القائمة نفسه، ستتفاجأ بأن المتغير cheese قد تغير رغم أننا عدلنا على المتغير spam وحده، ما يؤكّد أن تعليمة الإسناد في بايثون لا تنسخ الكائن نفسه وإنما المرجع إلى هذا الكائن؛ فتعليمة الإسناد cheese=spam تجعل من المتغير cheese يشير إلى نفس كائن القائمة الذي يشير إليه المتغير spam في ذاكرة الحاسب، وبالتالي لا تُستنسخ القائمة مرتين، وهذا ما يفسّر أن التعديل على المتغير spam سيطرأ أيضًا على المتغير cheese، إذ يشير كلاهما إلى نفس كائن القائمة. ينطبق المبدأ ذاته على الكائنات المتغيرة المُمررة إلى استدعاءات الدوال. لنكتب الشيفرة التالية في الصدفة التفاعلية ملاحظين أن كلًا من المتغير العام spam والمعامل المحلي theList يشيران إلى الكائن ذاته. تذكّر أن المعاملات parameters ما هي إلا متغيرات مُعرّفة ضمن تعليمة التصريح عن الدالة. >>> def printIdOfParam(theList): ... print(id(theList)) ... >>> eggs = ['cat', 'dog', 'eel'] >>> print(id(eggs)) 2356893256136 >>> printIdOfParam(eggs) 2356893256136 يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة على الرابط. نلاحظ أن الهويتين identities المُعادتين من التابع ()id متساويتان لكل من eggs و theList، ما يعني أن هذان المتغيران يشيران إلى كائن القائمة ذاته، أي أن كائن القائمة الخاص بالمتغير eggs لم يُنسخ إلى المتغير theList، وإنما المرجع إلى هذا الكائن هو الذي نُسخ، وهذا ما يفسر كون كلا المتغيرين يشيران إلى القائمة ذاتها، فلا يشغل المرجع سوى بضعة بايتات في الذاكرة، ولكن ماذا لو كانت بايثون تنسخ القائمة كاملةً بدلًا من نسخ المرجع فقط، فلو كان المتغير يتضمّن مليار عنصر بدلًا من ثلاثة فقط‘ فإن تمريره إلى الدالة ()printIdOfParam سيتطلب نسخ هذه القائمة الضخمة كاملةً، ما سيستهلك حتى عدة جيجا بايت من الذاكرة من أجل هذا الاستدعاء البسيط للدالة، وهذا ما يفسر سبب أن تعليمة الإسناد في بايثون تنسخ المراجع فقط ولا تنسخ الكائنات. إحدى طرق تجنب خطأ gotcha هذا تكون بأخذ نسخة عن كائن القائمة -وليس مرجعه فقط- باستخدام الدالة ()copy.copy. لنكتب ما يلي في الصدفة التفاعلية: >>> import copy >>> fish = [2, 4, 8, 16] >>> chicken = copy.copy(fish) >>> id(fish), id(chicken) (2356896337352, 2356896337480) >>> fish[0] = 'CHANGED' >>> fish ['CHANGED', 4, 8, 16] >>> chicken [2, 4, 8, 16] >>> id(fish), id(chicken) (2356896337352, 2356896337480) يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة على الرابط، وفيها يشير المتغير chicken إلى كائن قائمة مستقل منسوخ وليس على كائن القائمة الأصلي المشار إليه بالمتغير fish، وبذلك لن يحدث خطأ gotcha آنف الذكر. لكن وكما أن المتغيرات تمثّل عناوين أو أسماء وسوم تدل على الكائنات وليست صناديق محتوية على هذه الكائنات، فالقوائم تشابهها من حيث أنها تتضمن عناوين وأسماء وسوم دالة على الكائنات ولا تحتوي على الكائنات نفسها، فلو كان لدينا قائمة تحتوي على قوائم أخرى، فإن الدالة ()copy.copy تنسخ فقط مراجع تلك القوائم الداخلية. لنكتب الشيفرة التالية في الصدفة التفاعلية بغية الاطلاع على هذه المشكلة: >>> import copy >>> fish = [[1, 2], [3, 4]] >>> chicken = copy.copy(fish) >>> id(fish), id(chicken) (2356896466248, 2356896375368) >>> fish.append('APPENDED') >>> fish [[1, 2], [3, 4], 'APPENDED'] >>> chicken [[1, 2], [3, 4]] >>> fish[0][0] = 'CHANGED' >>> fish [['CHANGED', 2], [3, 4], 'APPENDED'] >>> chicken [['CHANGED', 2], [3, 4]] >>> id(fish[0]), id(chicken[0]) (2356896337480, 2356896337480) يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة على الرابط. بالرغم من كون كل من fish و chicken كائنا قوائم مختلفين، إلا أنهما يشيران إلى ذات القائمتين الداخليتين [1,2] و [3,4]، وبالتالي فإن تغيير أي منهما سيؤثّر على كلا المتغيرين، رغم استخدامنا للدالة ()copy.copy. يكمن الحل باستخدام الدالة ()copy.deepcopy، التي ستنسخ أي كائنات قوائم موجودة ضمن كائن القائمة الأصلي المنسوخ، وأي كائنات قوائم فرعية ضمن كائنات القوائم الداخلية في كائن القائمة الأصلي، وهكذا. لنكتب ما يلي في الصدفة التفاعلية: >>> import copy >>> fish = [[1, 2], [3, 4]] >>> chicken = copy.deepcopy(fish) >>> id(fish[0]), id(chicken[0]) (2356896337352, 2356896466184) >>> fish[0][0] = 'CHANGED' >>> fish [['CHANGED', 2], [3, 4]] >>> chicken [[1, 2], [3, 4]] يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة على الرابط. بالرغم من كون الدالة ()copy.deepcopy أبطأ قليلًا من الدالة ()copy.copy إلا أنها أأمن للاستخدام لحالات كونك غير متأكد من احتواء القائمة المنسوخة على قوائم داخلية أخرى، أو غيرها من الكائنات المتغيرة مثل القواميس والمجموعات. نصيحتنا العامة لك هي أن تستخدم الدالة ()copy.deepcopy دومًا، إذ أنها قد تجنبك وقوع أخطاء دقيقة، وغالبًا لن تلاحظ أصلًا البطء في تنفيذ الشيفرة الناتج عن استخدامها. لا تستخدم القيم المتغيرة من أجل وسطاء افتراضية تسمح بايثون بتعيين وسطاء افتراضية default arguments للمعاملات parameters لدى التصريح عن الدوال، فإذا لم يعين المستخدم معاملًا لدى استدعاء الدالة صراحةً، ستُنفّذ الدالة مُستخدمةً الوسيط الافتراضي مثل معامل، وهذا أمر مفيد خاصةُ لحالات كون معظم استدعاءات الدالة ستكون من أجل نفس الوسيط، إذ أن وجود الوسيط الافتراضي يجعل من تمرير معامل لدى استدعاء الدالة أمرًا اختياريًا؛ فعلى سبيل المثال، تمرير القيمة None إلى التابع ()split سيجعله يقتطع محارف المسافات البيضاء، ولكن ونظرًا لكون None هي أصلًا الوسيط الافتراضي لهذا التابع، إذ سيؤدي الاستدعاء ()cat dog'.split نفس الوظيفة كما لو كان بالشكل (cat dog'.split(None'، إذ تستخدم الدالة الوسيط الافتراضي معاملًا ما لم يُمرِّر المستخدم وسيطًا آخر. ينبغي عليك عدم تعيين أي من الكائنات المتغيرة مثل القائمة أو القاموس وسيطًا افتراضيًا. إذ سيؤدي ذلك إلى وقوع أخطاء كما هو موضح في المثال التالي، وفيه نصرح عن دالة باسم ()addIngredient تضيف سلسلةً نصيةً تمثّل مكونًا ما لقائمة تمثل شطيرة، وبما المكون الأول والأخير في الشطائر عادةً هو الخبز bread، استخدمنا القائمة المتغيرة ['bread', 'bread'] وسيطًا افتراضيًا: >>> def addIngredient(ingredient, sandwich=['bread', 'bread']): ... sandwich.insert(1, ingredient) ... return sandwich ... >>> mySandwich = addIngredient('avocado') >>> mySandwich ['bread', 'avocado', 'bread'] إلا أن استخدام كائنًا متغيّر مثل قائمة ['bread', 'bread'] وسيطًا افتراضيًا ينطوي على إشكال دقيق ألا وهو أن القائمة تُنشأ ولمرة واحدة لحظة تنفيذ التعليمة def المسؤولة عن التصريح عن الدالة، ولا تُنشأ من أجل كل استدعاء جديد للدالة، ما يعني أن كائن قائمة ['bread', 'bread'] وحيد سيُنشأ، كوننا لا نصرّح عن الدالة ()addIngredient سوى لمرة واحدة، إلا أن كل استدعاء جديد لهذه الدالة سيستخدم نفس القائمة، ما سيؤدي إلى سلوك خاطئ غير متوقع، كما يلي: >>> mySandwich = addIngredient('avocado') >>> mySandwich ['bread', 'avocado', 'bread'] >>> anotherSandwich = addIngredient('lettuce') >>> anotherSandwich ['bread', 'lettuce', 'avocado', 'bread'] في الشيفرة السابقة وبما أن الاستدعاء ('addIngredient('lettuce سيستخدم نفس القائمة المُعيّنة وسيطًا افتراضيًا كما هو الحال في الاستدعاءات السابقة، التي أُضيفت إليها هذه القائمة أصلًا على العنصر avocado، لذا وبدلًا من الحصول على قائمة بالشكل: ['bread', 'lettuce', 'bread']، ستعيد الدالة القائمة: ['bread', 'lettuce', 'avocado', 'bread']؛ إذ تظهر السلسلة النصية avocado مجددًا لأن القائمة الخاصة بالمعامل sandwich هي نفسها من الاستدعاء السابق للدالة، فلا تُنشأ سوى قائمة ['bread', 'bread'] واحدة، نظرًا لأن تعليمة def الخاصة بالتصريح عن الدالة لا تُنفّذ سوى مرة واحدة، ولا تُنفّذ من أجل كل استدعاء جديد للدالة. يمكنك الاطلاع على تمثيل مرئي لتنفيذ الشيفرة السابقة على الرابط. يكمن الحل البايثوني لدى حاجتك لتعيين قائمة أو قاموس وسيطًا افتراضيًا، في تعيين قيمة هذا الوسيط إلى None، ثم جعل شيفرتك تتحقق من إعادة هذه القيمة لتُنشئ عندها قائمة أو قاموس جديد من أجل كل استدعاء جديد للدالة، وهذا يضمن إنشاء الدالة لكائن مُتغير جديد من أجل كل استدعاء للدالة بدلًا من إنشائه لمرة واحدة عند التصريح عن الدالة، كما في المثال التالي: >>> def addIngredient(ingredient, sandwich=None): ... if sandwich is None: ... sandwich = ['bread', 'bread'] ... sandwich.insert(1, ingredient) ... return sandwich ... >>> firstSandwich = addIngredient('cranberries') >>> firstSandwich ['bread', 'cranberries', 'bread'] >>> secondSandwich = addIngredient('lettuce') >>> secondSandwich ['bread', 'lettuce', 'bread'] >>> id(firstSandwich) == id(secondSandwich) 1 False نلاحظ أن المعاملين firstSandwich وsecondSandwich لا يتشاركان مرجع القائمة ذاتها كما هو موضح في السطر رقم 1 من الشيفرة السابقة، ذلك لأن التعليمة ['sandwich = ['bread', 'bread تُنشئ كائن قائمة جديد في كل مرة تُستدعى فيها الدالة ()addIngredient، بدلًا من إنشائها لمرة واحدة لحظة التصريح عن الدالة. تتضمن أنماط البيانات المتغيرة كلًا من القوائم والقواميس والمجموعات وجميع الكائنات المُصرّح عنها باستخدام التعليمة class، فلا تستخدم كائنات من هذه الأنماط وسطاء افتراضية ضمن تعليمة def الخاصة بالتصريح عن الدوال. لا تبن السلاسل النصية باستخدام عمليات ربط السلاسل النصية String Concatenation تعد السلاسل النصية في بايثون كائناتٍ ثابتة immutable، ما يعني أنه لا يمكن تغيير قيم السلاسل النصية، وأي شيفرة تبدو وكأنها تُعدّل على سلسلة نصية، فإنها في الواقع تُنشئ كائن سلسلة نصية جديد؛ فعلى سبيل المثال، كل من العمليات التالية تُغير من محتوى المتغير spam، الأمر الذي يحدث باستبدال محتواه بسلسلة نصية جديدة ذات هوية جديدة وليس بالتعديل على قيمة السلسلة الموجودة أصلًا: >>> spam = 'Hello' >>> id(spam), spam (38330864, 'Hello') >>> spam = spam + ' world!' >>> id(spam), spam (38329712, 'Hello world!') >>> spam = spam.upper() >>> id(spam), spam (38329648, 'HELLO WORLD!') >>> spam = 'Hi' >>> id(spam), spam (38395568, 'Hi') >>> spam = f'{spam} world!' >>> id(spam), spam (38330864, 'Hi world!') من الجدير بالملاحظة أن كل استدعاء للدالة (id(spam يعيد هويةً جديدةً، والسبب وراء ذلك هو أن كائن السلسلة النصية الموجود في المتغير spam لا يُعدَّل، وإنما يُستبدل كاملًا بكائن سلسلة نصية جديد بهوية مختلفة، كما أن إنشاء سلاسل نصية جديدة باستخدام أي من بناء السلاسل النصية f-strings أو التابع ()format أو المحدد s% يُنشئ كائنات سلاسل نصية جديدة، كما هو الحال لدى ربط السلاسل النصية. قد لا تهمنا هذه التفاصيل التقنية في الأحوال العادية، إذ أن بايثون لغة برمجة عالية المستوى والتي تأخذ على عاتقها إنجاز العديد من التفاصيل المشابهة بدلًا عنك، سانحةً لك المجال لتصب تركيزك فقط على إنشاء برنامجك، إلا أن بناء سلسلة نصية باستخدام عدد كبير من عمليات ربط السلاسل النصية قد يتسبب بإبطاء برنامجك، فمع كل تكرار للحلقة سيُنشَئ كائن سلسلة نصية جديد ليُحذف سابقه، أي لدى استخدام عملية ربط سلاسل نصية ضمن حلقة for أو while كما في المثال التالي: >>> finalString = '' >>> for i in range(100000): ... finalString += 'spam ' ... >>> finalString spam spam spam spam spam spam spam spam spam spam spam spam --snip– بما أن العملية ' finalString += 'spam ستُنفّذ 100000 مرة ضمن الحلقة، ستجري بايثون 100000 عملية ربط سلاسل محرفية، إذ ستُنشئ وحدة المعالجة المركزية CPU قيم السلاسل النصية الوسيطة بربط القيمة الحالية للمتغير finalString عند كل تكرار مع السلسلة 'spam'، لتخزّن السلسلة الناتجة في الذاكرة، لتعود وتحذفها من الذاكرة مجددًا عند التكرار الجديد للحلقة، ما يمثل مقدارًا كبيرًا من الجهد الضائع، فكل ما يهمنا هو الحصول على السلسلة النصية النهائية بالنتيجة. تكمن الطريقة البايثونية في بناء السلاسل النصية بإضافة السلاسل الفرعية الأقصر إلى قائمة ومن ثم دمجها معًا وصولًا إلى سلسلة نصية واحدة. رغم كون هذه الطريقة تُنشئ أيضًا 100000 كائن سلسلة نصية، إلا أنها لا تجري سوى عملية ربط سلاسل نصية واحدة وذلك عند استدعاء الدالة ()join؛ فعلى سبيل المثال، تُنشئ الشيفرة التالية سلسلة نهائية في المتغير finalString مكافئة لتلك في المثال السابق ولكن دون استخدام عمليات ربط وسيطة للسلاسل النصية، على النحو التالي: >>> finalString = [] >>> for i in range(100000): ... finalString.append('spam ') ... >>> finalString = ''.join(finalString) >>> finalString spam spam spam spam spam spam spam spam spam spam spam spam --snip– لدى قياس زمن التنفيذ لكلا الشيفرتين السابقتين وعلى الحاسب نفسه، وجدنا أن المنهجية الثانية المتمثلة باستخدام قائمة لإضافة السلاسل النصية المراد ربطها إليها أسرع بعشر مرات من منهجية ربط السلاسل المحرفية بالعوامل، وسيغدو الفرق في السرعة هذا أكبر وأوضح مع زيادة عدد التكرارات المطلوبة، ولكن حتى لحالة التكرار 100 مرة عوضًا عن 100000، ورغم كون منهجية ربط السلاسل الأولى تبقى أبطأ من منهجية الإضافة إلى قائمة، إلا أن الفرق في هذه الحالة ضئيل ويمكن إهماله؛ ما يعني أنه لا يجب إلغاء استخدام ربط السلاسل النصية تمامًا سواء باستخدام f-strings أو التابع ()format أو المعرف s% لكل الحالات، فالسرعة تتحسن بصورة ملحوظة فقط من أجل عدد ضخم من عمليات الربط. تخفف عنك بايثون عبء التفكير والاهتمام بالكثير من التفاصيل التي تجري في الخلفية، سامحةً للمبرمجين بكتابة برامجهم بسرعة، وكما ذكرنا سابقًا فإن وقت المبرمج أثمن من وقت وحدة المعالجة المركزية، ومع ذلك من الجيد فهم التفاصيل في بعض الحالات، مثل الفرق ما بين السلاسل النصية الثابتة والقوائم المتغيرة، ما يجنبنا الوقوع في فخ أخطاء gotcha، كما في حالة بناء السلاسل النصية عبر ربطها. لا تتوقع من التابع ()sort أن يرتب القيم أبجديا لعل فهم خوارزميات الترتيب -الخوارزميات المسؤولة عن ترتيب القيم بطريقة ممنهجة وفقًا لترتيب أو فرز محدد- أمر أساسي ومهم في دراسة علم الحاسوب، إلا أن ما بين يديك الآن هو ليس كتاب في علم الحاسوب، ما قد يوحي بعدم أهمية معرفتك لهذه الخوارزميات، طالما أنه من الممكن استدعاء تابع الترتيب في بايثون ()sort ببساطة، ولكن ستلاحظ أن لهذا التابع بعض السلوكيات الغريبة أحيانًا في ترتيب البيانات، كأن يضع ترتيب حرف Z (الأخير في الأبجدية الإنجليزية) وهو في حالته الكبيرة قبل حرف a (الأول في الأبجدية الإنجليزية) وهو في حالته الصغيرة، كما في المثال: >>> letters = ['z', 'A', 'a', 'Z'] >>> letters.sort() >>> letters ['A', 'Z', 'a', 'z'] الترميز الأمريكي المعياري لتبادل المعلومات American Standard Code for Information Interchange -أو اختصارًا ASCII- ويُقرأ أسكي هو جدول يربط ما بين الترميزات الرقمية numeric codes (وتسمى بنقاط الترميز code points أو الأعداد الترتيبية ordinals) والمحارف النصية، ويرتب التابع ()sort القيم وفقًا لترتيب ترميز أسكي (ASCII-betical وهو مصطلح شائع ويعني الفرز وفق ترتيب عددي موافق لقيم ترميز الأسكي) وليس وفق ترتيب أبجدي. فوفقًا لترميز الأسكي نقطة الترميز الموافقة للحرف A هي 65، وللحرف B هي 66 وهكذا حتى الحرف Z الموافق لنقطة الترميز 90، أما الحرف a (الحرف A في حالته الصغيرة) فيوافق 97 والحرف b يوافق 98 وهكذا حتى الحرف z الموافق لنقطة الترميز 122، وبالتالي ولدى الفرز وفقًا لترميز الأسكي سيأتي الحرف Z (ذو نقطة الترميز 90) قبل الحرف a (ذو نقطة الترميز 97). رغم كون ترميز الأسكي هو الأشيع في مجال الحوسبة عند الغرب ما قبل وخلال التسعينيات، ولكن يبقى هذا الترميز أمريكيًا فقط، إذ يوجد نقطة ترميز لعلامة الدولار $ وهي 36، ولكن لا يوجد نقطة ترميز لعلامة الجنيه البريطاني £، وبالتالي استُبدل ترميز الأسكي على نطاقٍ واسع بترميز Unicode، إذ يتضمّن كافة نقاط ترميز الأسكي إضافةً إلى ما يزيد عن 100000 نقطة ترميز أخرى. يمكن معرفة نقطة الترميز الموافقة لمحرف ما بتمريره إلى الدالة ()ord. كما يمكن معرفة المحرف الموافق لنقطة ترميز ما بتمريره العدد الصحيح الموافق لنقطة الترميز إلى الدالة ()chr، التي تعيد سلسلةً نصيةً تتضمن المحرف الموافق، فعلى سبيل المثال، لنكتب الشيفرة التالية في الصدفة التفاعلية: >>> ord('a') 97 >>> chr(97) 'a' أما في حال الرغبة بالفرز وفق ترتيب أبجدي، نمرر التابع str.lower إلى المعامل key من التابع ()sort، وبالتالي تُفرز القيم كما لو أنها مُررت إلى الدالة ()lower قبل فرزها وترتيبها: >>> letters = ['z', 'A', 'a', 'Z'] >>> letters.sort(key=str.lower) >>> letters ['A', 'a', 'z', 'Z'] نلاحظ في الشيفرة السابقة أن السلاسل النصية ضمن القائمة لم تحوّل إلى حالة الأحرف الصغيرة، وإنما فقط فُرزت كما لو أنها كانت كذلك. يؤمن نيد باتشيلدرNed Batchelder مزيدًا من المعلومات حول يونيكود Unicode ونقاط الشيفرة في حديثه عن يونيكود البراغماتي أو كيف يمكنني إيقاف الألم من خلال الرابط. من الجدير بالذكر أن خوارزمية الفرز التي يستخدمها التابع ()sort هي Timsort،المصممة من قبل تيم بيترز Tim Peters مطوّر نواة بايثون ومؤلف مبادئ بايثون التوجيهية العشرون (Zen of Python)، وهي خوارزمية هجينة من الفرز بالدمج والفرز بالإدراج insertion. لا تفترض أن الأعداد ذات الفاصلة العشرية دقيقة تماما لا تستطيع الحواسيب تخزين الأعداد سوى في نظام العد الثنائي، المكون من الخانتين 1 و0 فقط. ولعرض الأرقام ذات الفاصلة العشرية بالشكل المألوف بالنسبة لنا، لا بد من ترجمة رقم مثل 3.14 إلى سلسلة من الأصفار والواحدات، وتجري الحواسيب عملية التحويل هذه وفقًا للمعيار IEEE 754، المنشور من قبل معهد مهندسي الكهرباء والإلكترونيات (IEEE والتي تُقرأ آي تربل إي). لتبسيط الأمور فإن هذه التفاصيل مخفية على المبرمجين، سامحةً لنا بكتابة الأعداد مع الفاصلة العشرية دون التفكير بعملية التحويل من النظام العشري إلى الثنائي: >>> 0.3 0.3 ورغم كون تفاصيل بعض الحالات هي خارج اهتمامات كتابنا هذا، لن يطابق تمثيل IEEE 754 للأعداد ذات الفاصلة العشرية تمامًا القيمة الموافقة في النظام العشري دومًا. أحد أشهر الأمثلة هو العدد 0.1، على النحو التالي: >>> 0.1 + 0.1 + 0.1 0.30000000000000004 >>> 0.3 == (0.1 + 0.1 + 0.1) False وهذا المجموع الغريب غير الدقيق ناتج عن أخطاء تقريب الناتجة عن كيفية تمثيل الحواسيب ومعالجتها للأعداد ذات الفاصلة العشرية، وهذا ليس خطأ gotcha خاص ببايثون، إذ أن المعيار IEEE 754 مطبق مباشرةً على دارات احتساب الفاصلة العشرية في وحدة المعالجة المركزية، وبالتالي سنحصل على نتيجة مشابهة لو استخدمنا لغات برمجة أخرى مثل ++C أو جافا سكريبت JavaScript أو أي لغة برمجة عاملة على وحدة معالجة مركزية تستخدم المعيار IEEE 754، وفي الواقع هو المعيار المطبق على كل وحدات المعالجة المركزية حول العالم، كما أن المعيار IEEE 754 ولأسبابٍ خارج اهتمامات كتابنا هذا غير قادر أيضًا على تمثيل كافة الأعداد الصحيحة التي تزيد عن 253. فمثلًا كلا القيمتين 253 و 253+1 مثل أعداد حقيقية (من النوع float) تقربان إلى القيمة 9007199254740992.0 ذاتها: >>> float(2**53) == float(2**53) + 1 True طالما أنك تستخدم نمط بيانات الفاصلة العشرية، فما من مفر من أخطاء التقريب هذه، ولكن لا تقلق، فطالما أنك لا تكتب برنامجًا لأحد المصارف أو لمفاعل نووي أو لمفاعل نووي خاص بأحد البنوك فإن أخطاء التقريب هذه ستكون صغيرة بما يكفي بحيث أنها لن تتسبب بمشاكل جوهرية في برامجك، ومن الممكن غالبًأ تجاوز هذه المشكلة بالاعتماد على الأعداد الصحيحة مع وحدات أصغر، كأن نستخدم 133 سنتًا بدلًا من 1.33 دولارًا، أو 200 ميللي ثانية بدلًا من 0.2 ثانية، وعلى هذا النحو تُجمع القيم 10+10+10 لتعطي 30 سنتًا أو ميللي ثانية بدلًا من جمع 0.1+0.1+0.1 لتعطي 0.30000000000000004 دولارًا أو ثانية. أما في حال الحاجة لدقة متناهية، مثلًا لحسابات علمية أو مالية، فمن الممكن استخدام وحدة decimal المبنية مُسبقًا في بايثون، ورغم كون كائنات هذه الوحدة أبطأ، إلا أنها بديل دقيق للقيم الحقيقية العشرية، فعلى سبيل المثال، التعليمة ('decimal.Decimal('0.1 تُنشئ كائنًا يمثل العدد 0.1 تمامًا بعيدًا عن خطأ التقريب الذي ينطوي عليه العدد ذاته كقيمة عشرية من النوع float. سيُنشئ تمرير القيمة الحقيقية 0.1 إلى ()decimal.Decimal كائن Decimal بنفس خطأ التقريب كما لو كان قيمة حقيقية عادية، وهذا ما يفسر كون القيمة الناتجة لا تساوي تمامًا القيمة ('Decimal('0.1. لذا يجب تمرير القيم الحقيقية مثل سلاسل نصية إلى ()decimal.Decimal، ولتوضيح هذه النقطة، لنكتب التالي في الصدفة التفاعلية: >>> import decimal >>> d = decimal.Decimal(0.1) >>> d Decimal('0.1000000000000000055511151231257827021181583404541015625') >>> d = decimal.Decimal('0.1') >>> d Decimal('0.1') >>> d + d + d Decimal('0.3') ليس لدى الأعداد الصحيحة أخطاء تقريب، لذا من الآمن تمريرها كما هي إلى ()decimal.Decimal. والآن لنكتب التالي في الصدفة التفاعلية: >>> 10 + d Decimal('10.1') >>> d * 3 Decimal('0.3') >>> 1 - d Decimal('0.9') >>> d + 0.1 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'decimal.Decimal' and 'float' ولكن كائنات Decimal لا تمتلك دقة غير متناهية، إذ أنها ببساطة تمتلك مستوى دقة محدد ومعرّف. لنأخذ العمليات التالية كمثال: >>> import decimal >>> d = decimal.Decimal(1) / 3 >>> d Decimal('0.3333333333333333333333333333') >>> d * 3 Decimal('0.9999999999999999999999999999') >>> (d * 3) == 1 # d is not exactly 1/3 False يُقيّم في الشيفرة السابقة التعبير البرمجي decimal.Decimal(1) / 3 إلى قيمة لا تساوي تمامًا الثلث. وافتراضيًا يكون بمستوى 28 رقمًا بارزًا significant، ومن الممكن الاطلاع على عدد الأرقام البارزة التي تستخدمها الوحدة decimal باستخدام السمة decimal.getcontext().prec (إذ أن prec تقنيًا هي سمة للكائن المعاد من التابع ()getcontext، ولكن من المناسب استخدامها في نفس السطر مع التابع). يمكنك تغيير هذه السمة وبالتالي مستوى الدقة بتغيير عدد الأرقام البارزة إلى ذلك المحدد من قبلك، ففي المثال التالي في الصدفة التفاعلية، سنقلل عدد الأرقام البارزة من 28 إلى 2 على النحو التالي: >>> import decimal >>> decimal.getcontext().prec 28 >>> decimal.getcontext().prec = 2 >>> decimal.Decimal(1) / 3 Decimal('0.33') إذ توفر الوحدة decimal لنا القدرة على التحكم بكيفية تعاطي الأرقام مع بعضها بعضًا. لا تستخدم سلسلة من عوامل عدم التساوي =! يمثّل استخدام سلسلة من عوامل المقارنة مثل 18 < age < 35 أو سلسلة من عوامل الإسناد مثل six = halfDozen = 6 اختصارًا مفيدًا للتعابير: ( age > 18 ) and (age < 35) six = 6; halfDozen = 6 على التوالي، ولكن لا ينبغي استخدام سلسلة من عوامل عدم التساوي =!، فقد تظن أن الشيفرة التالية تتحقق من كون المتغيرات الثلاث تمتلك قيمًا مختلفة عن بعضها بعضًا، نظرًا لأن التعبير قد قُيّم على أنه صحيح True: >>> a = 'cat' >>> b = 'dog' >>> c = 'moose' >>> a != b != c True إلا أن السلسلة السابقة في الواقع تكافئ (a != b) and (b != c)، ما يعني أنه من الممكن كون المتغير a مساويًا للمتغير c وسيُقيم التعبير a != b != c أيضًا على أنه صحيح True، على النحو التالي: >>> a = 'cat' >>> b = 'dog' >>> c = 'cat' >>> a != b != c True وهو خطأ دقيق ومعه ستكون الشيفرة مُضلِلة، لذا من الأفضل تجنب استخدام سلسلة من عوامل عدم المساواة =! معًا. لا تنس استخدام الفاصلة في الصفوف وحيدة العنصر لدى استخدام الصفوف في الشيفرات، يجب أن نأخذ بالحسبان وضع فاصلة لاحقة زائدة حتى وإن كان الصف يحتوي على عنصر وحيد. ففي حين أن القيمة ( ,42) تُمثّل صفًا يحتوي على العدد الصحيح 42، فإن القيمة (42) تمثّل العدد الصحيح 42 نفسه؛ فالأقواس في (42) مشابهة لتلك في التعبير (20+1)*2 والذي يُقيّم إلى القيمة 42. فنسيان الفاصلة في الصف قد يؤدي لما يلي: >>> spam = ('cat', 'dog', 'moose') >>> spam[0] 'cat' >>> spam = ('cat') 1 >>> spam[0] 'c' 2 >>> spam = ('cat', ) >>> spam[0] 'cat' فبدون الفاصلة سيُقيّم ('cat') إلى قيمة سلسلة محارف، ما يفسر أن التعبير [spam[0 المشار إليه في السطر رقم 1 يُقيّم إلى المحرف الأول من السلسلة وهو c؛ فالفاصلة الزائدة مطلوبة ضمن القوسين حتى يجري التعرف عليها مثل صف كما في السطر رقم 2 من الشيفرة السابقة. يميز استخدام الفاصلة في بايثون الصف عن الأقواس المجردة. الخلاصة قد يحدث سوء التواصل في أي لغة بما في ذلك في لغات البرمجة، ولدى بايثون بضعة صيغ تبدو صحيحة إلا أنها قد تتسبب بحدوث أخطاء نسميها gotchas والتي يمكن أن يقع في فخها المبرمج غير المحترف، ورغم ندرتها إلا أنه من الضروري معرفتها بما يسمح بملاحظتها والتعرف عليها عند وقوعها وتنقيح المشاكل الناجمة عنها. بالرغم أنه من الممكن تقنيًا إضافة العناصر أو حذفها من قائمة ما أثناء المرور عليها، إلا أن هذه الآلية تُشكّل مصدرًا محتملًا للأخطاء، والحل الآمن هنا يكون بالتكرار على نسخة من القائمة، ثم إجراء التغييرات على القائمة الأصلية. من الجدير بالذكر أنه عند إنشاء نسخة عن قائمة (أو عن أي كائن قابل للتغيير)، فإن تعليمة الإسناد تنسخ فقط المرجع إلى هذا الكائن، وليس الكائن نفسه، وفي حال رغبتك بنسخ الكائن (مع أي كائنات فرعية يشير إليها) فيمكنك استخدام الدالة ()copy.deepcopy. ينبغي عليك عدم استخدام الكائنات المتغيرة مثل وسطاء افتراضية ضمن عبارة def الخاصة بالتصريح عن الدوال، إذ بذلك لن يُنشأ هذا الوسيط سوى مرة واحدة لحظة تنفيذ التعليمة def وليس في كل مرة تُستدعى فيها الدالة، والحل الأفضل يكون بتعيين الوسيط الافتراضي ليكون None، مع إضافة شيفرة لتتحقق من تمرير None بالفعل مثل وسيط لتُنشئ عندها الكائن المتغير المطلوب عند استدعاء الدالة. ينتج أحد أخطاء gotcha عن بناء سلسلة نصية عبر ربط العديد من السلاسل النصية الأصغر باستخدام العامل + ضمن حلقة. فمن أجل عدد صغير من التكرارات تعد الصياغة السابقة مقبولة. ولكن خلف الكواليس وباستخدام هذه الطريقة تضطر بايثون لإنشاء كائنات سلاسل نصية وإعادة حذفها على التوالي من أجل كل تكرار. الحل الأفضل يكون بإضافة السلاسل النصية الأصغر إلى قائمة ومن ثم المعامل ()join لإنشاء السلسلة النهائية المطلوبة. يفرز التابع ()sort القيم اعتمادًا على نقاط الترميز المختلفة عن الترتيب الأبجدي، إذ يُرتّب الحرف Z في حالته الكبيرة قبل الحرف a في حالته الصغيرة، ويمكن حل هذه المشكلة باستخدام الاستدعاء على النحو (sort(key=str.lower. تمتلك الأعداد العشرية خطأ تقريب بسيط ناتج عن طريقة تمثيل الأعداد، وهذا الأمر عديم الأهمية في معظم البرامج، وإذا كان الأمر ذو تأثير على برنامجك، فمن الممكن استخدام وحدة بايثون decimal. نهايةً، تجنّب تمامًا ربط عوامل عدم التساوي =!، لأن التعبير 'cat' != 'dog' != 'cat' سيُقيّم على نحوٍ غريب على أنه صحيح True. رغم أن هذا المقال يوصف أخطاء gotcha في بايثون التي قد تواجهها، إلا أن هذا النوع من الأخطاء قليل الحدوث في معظم الشيفرات الواقعية، وقد بذلت بايثون مجهودًا كبيرًا في تقليل المفاجآت التي قد تصادفك في برامجك. سنغطي في المقال التالي بعضًا من أخطاء gotcha الأندر والأغرب، ويكاد يكون من المستحيل أن تواجه هذه السلوكيات الغريبة ما لم تبحث عنها متعمدًا، ومع ذلك يبقى من الممتع الاطلاع عليها ومعرفة أسباب حدوثها. ترجمة -وبتصرف- للفصل الثامن "البنى الصحيحة المؤدية إلى الأخطاء Gotchas الشائعة في بايثون" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: مصطلحات شائعة مثيرة للالتباس في بايثون الطرق البايثونية في استخدام قواميس بايثون ومتغيراتها وعاملها الثلاثي تعلم لغة بايثون
-
المصطلحات التقنية مربكة كفاية، فما بالك بمصطلحات متعلقة ببعضها وذات تعاريف مختلفة بنفس الوقت، وما يجعل الأمور تزداد سوءًا أن لغات البرمجة وأنظمة التشغيل ومجالات الحوسبة عمومًا قد تستخدم مصطلحات مختلفة لتوصيف الشيء ذاته أو تستخدم نفس المصطلحات لتوصيف أشياء مختلفة. لا بُد من التمييز ما بين المصطلحات التالية بما يضمن لك التواصل الواضح مع المبرمجين الآخرين. سنتعرّف في هذا المقال على مجموعة من المصطلحات التي قد تثير الالتباس في فهمها لنتجنّب الخلط فيما بينها. التعليمات البرمجية Statements والتعابير البرمجية Expressions التعابير البرمجية هي تعليمات مكونة من عوامل وقيمٍ والتي تُقيّم إلى قيمة وحيدة، ويمكن لهذه القيمة أن تكون متغيرًا (يحتوي على قيمة) أو استدعاء دالة (يعيد قيمة)؛ فمثلًا 2+2 هو تعبير برمجي إذ يُقيّم إلى القيمة الوحيدة 4، كما أن كلًا من len(myName) > 4 و ()myName.isupper أو 'myName == 'Zophie هي تعابير برمجية أيضًا. تُعد القيمة الوحيدة بحد ذاتها تعبيرًا برمجيًا أيضًا، إذ تُقيّم إلى نفسها. أما التعليمات البرمجية فهي كل ما تبقى من أوامر في بايثون (باستثناء التعابير البرمجية)، والتي تتضمّن تعليمات الجمل الشرطية if والحلقات التكرارية for وتعليمات التصريح عن الدوال def وتعليمات القيم المعادة return وغيرها، إذ لا تُقيّم التعليمات البرمجية إلى قيمة وحيدة، وقد تحتوي بعض التعليمات البرمجية على تعابير برمجية، كما في تعليمات الإسناد مثل spam = 2 + 2 أو تعليمات الشرط مثل 'myName == 'Zophie. يستخدم الإصدار الثالث من بايثون الدالة ()print، بينما يستخدم الإصدار الثاني منها التعليمة print بدلًا عنها، وقد يبدو أن الفرق بينهما هو فقط استخدام الأقواس أو عدمه، ولكن من المهم ملاحظة أن الدالة ()print في الإصدار الثالث من بايثون تتضمّن قيمة معادة (وهي تساوي دومًا None) كما من الممكن تمريرها وسيطًا للدوال الأخرى ويمكن إسنادها إلى المتغيرات، في حين أن كل من الإجراءات السابقة غير متاح لدى التعامل مع التعليمات. مع ذلك، يمكن استخدام الأقواس مع تعليمة print في الإصدار الثاني من بايثون كما في المثال التالي: >>> print 'Hello, world!' # run in Python 2 Hello, world! 1 >>> print('Hello, world!') # run in Python 2 Hello, world! يبدو السطر ذو الرقم 1 مثل استدعاء دالة، إلا أنه تعليمة print مع سلسلة نصية محصورة بين قوسين، وكذلك تعليمة الإسناد (spam = (2 + 2 تكافئ spam = 2 + 2. يمكن في كل من الإصدارين 2 و 3 من بايثون تمرير قيم متعددة إلى التعليمة print أو الدالة ()print على التوالي. سيبدو الأمر في الإصدار 3 من يابثون بالشكل التالي: >>> print('Hello', 'world') # run in Python 3 Hello world لو استخدمنا نفس الشيفرة السابقة ولكن في الإصدار الثاني من بايثون، ستفسّر التعليمة على أنها تمرير لصف مكون من سلسلتين نصيتين ضمن تعليمة print، لينتج الخرج التالي: >>> print('Hello', 'world') # run in Python 2 ('Hello', 'world') إذًا، للتعليمات البرمجية والتعابير البرمجية المكونة من استدعاء تابع اختلافات دقيقة جدًا إلا أنها حقيقية. الكتل Block والبنية Clause والمتن Body تُستخدم عادةً كل من مصطلحات الكتلة والبنية والمتن تبادليًا للإشارة إلى مجموعة من تعليمات بايثون. تبدأ الكتل بمسافة بادئة وتنتهي عندما تصبح المسافة البادئة بمستوى تلك السابقة، فعلى سبيل المثال، تدعى الشيفرات التالية لتعليمة if أو لتعليمة for بكتلة التعليمات، ولا بُد من وجود كتلة جديدة بعد التعليمات المنتهية بنقطتين رأسيتين :، مثل تعليمات if و else و for و while و def و class وغيرها. تسمح بايثون بكتل السطر الواحد، فهي تعمل رغم كونها غير منصوح بها في صيغة بايثون: if name == 'Zophie': print('Hello, kitty!') ومع استخدام الفاصلة المنقوطة، يصبح من الممكن استخدام عدة أوامر ضمن كتلة التعليمة if، على النحو التالي: if name == 'Zophie': print('Hello, kitty!'); print('Do you want a treat?') لكن من غير المسموح استخدام نفس السطر لكتابة تعليمات أخرى تتطلب بدورها كتلًا جديدة، فالشيفرة التالية مثلًا غير صالحة في بايثون: if name == 'Zophie': if age < 2: print('Hello, kitten!') وسبب رفض هذه الصيغة هو في حال وجود تعليمة else في السطر التالي، سيكون من الصعب معرفة كونها تابعة لأي من تعليمتي if. يفضّل توثيق بايثون الرسمي استخدام مصطلح البنية بدلًا من الكتلة، والشيفرة التالية تمثّل بنية: if name == 'Zophie': print('Hello, kitty!') print('Do you want a treat?') تمثّل التعليمة if ترويسة البنية، بينما يمثّل الاستدعائين المتداخلين للدالة ()print ضمن if متن البنية. يستخدم توثيق بايثون الرسمي مصطلح الكتلة للإشارة إلى جزء من شيفرات بايثون يُنفّذ مثل وحدة واحدة كما في حالات التعريف عن وحدة أو دالة أو صنف. المتغير Variable والسمة Attribute المتغيرات ببساطة هي أسماء تشير إلى الكائنات، أما السمات وفقًا لتوثيق بايثون الرسمي فهي أي اسم يتبع رمز النقطة، وترتبط السمات بالكائنات (وهي الأسماء التي تسبق النقطة). لنكتب على سبيل المثال الشيفرات التالية في الصدفة التفاعلية: >>> import datetime >>> spam = datetime.datetime.now() >>> spam.year 2018 >>> spam.month 1 يمثل spam في الشيفرة السابقة متغيرًا يتضمّن كائنًا من نوع datetime (والمُعاد من التعليمة ()datetime.datetime.now)، في حين أن كل من year و month هي سمات لهذا الكائن. حتى لو كتبنا على سبيل المثال التعليمة ()sys.exit، ففي هذه الحالة تمثّل الدالة ()exit سمةً لكائن الوحدة sys. تسمي بعض لغات البرمجة الأخرى السمات بالخاصيات properties أو صفات المتغيرات member variables. الدوال Function والتوابع Method الدالة هي مجموعة من الشيفرات التي تُشغَّل عند استدعائها؛ أما التابع فهو دالة (أو شيء قابل للاستدعاء callable، الأمر الذي سنشرحه في الفقرة التالية) مرتبطة بصنف ما، تمامًا مثل مبدأ كون السمات متغيرات مرتبطة بكائنات. تشمل الدوال تلك الموجودة أصلًا في المكتبة المبنية مسبقًا للغة built-in functions وتلك المخصصة للوحدات. لنكتب على سبيل المثال ما يلي في الصدفة التفاعلية: >>> len('Hello') 5 >>> 'Hello'.upper() 'HELLO' >>> import math >>> math.sqrt(25) 5.0 في الشيفرة أعلاه، ()len هي دالة، بينما ()upper هو تابع للسلاسل النصية. تُعد التوابع مثل سمات للكائنات المرتبطة بها. وتجدر الملاحظة أن وجود النقطة لا يعني بالضرورة أن ما يليها تابعًا وليس دالة، ففي مثالنا السابق الدالة ()sqrt مرتبطة بالوحدة math، إذ أن math ليست صنفًا. القابل للتكرار Iterable والمكرر Iterator حلقة for التكرارية في بايثون متعددة الاستعمالات، إذ ستشغّل التعليمة (for i in range(3 كتلة من الشيفرة لثلاث مرات، فالاستدعاء (3)range ليس مجرد طريقة في بايثون لإعلام حلقة for برغبتنا بتكرار جزء من الشيفرة لثلاث مرات، لأن استدعائها يعيد كائن نطاق، تمامًا كما يعيد الاستدعاء ('list('cat كائن قائمة. كل من الكائنين السابقين أمثلة عن الكائنات القابلة للتكرار. نستخدم الكائنات القابلة للتكرار في حلقات for التكرارية. لنكتب الشيفرات التالية في الصدفة التفاعلية بغية مشاهدة كيفية مرور الحلقة for على كائن نطاق وكائن قائمة: >>> for i in range(3): ... print(i) # body of the for loop ... 0 1 2 >>> for i in ['c', 'a', 't']: ... print(i) # body of the for loop ... c a t كما تتضمن الكائنات القابلة للتكرار كافة الأنماط المتسلسلة من قبيل كائنات النطاق والقائمة والصف والسلاسل النصية، وبعضًا من كائنات الحاويات مثل كائنات القاموس والمجموعة والملف. يحدث الكثير في كواليس أمثلة حلقات for هذه، إذ يستدعي بايثون كلًا من الدالتين ()iter و ()next المبنية مسبقًا لعمل حلقة for؛ فلدى استخدام حلقة for تُمرر الكائنات المُكرَّرة إلى الدالة المبنية مسبقًا ()iter، والتي تعيد كائناتٍ مكرِّرة. رغم كون الكائنات المكرَّرة تتضمن العناصر، إلا أن الكائن المكرِّر هو الذي يتتبع الكائن الواجب استخدامه في الخطوة التالية من الحلقة، فمن أجل كل تكرار للحلقة، يُمرَّر الكائن المكرِّر إلى الدالة ()next المبنية مسبقًا، لتعيد العنصر التالي من العناصر المكرَّرة. يمكن استخدام الدالتين ()iter و ()next يدويًا لمشاهدة كيفية عمل حلقات for التكرارية مباشرةً. لنكتب الشيفرات التالية في الصدفة التفاعلية لإنجاز نفس الأوامر كما في المثال السابق: >>> iterableObj = range(3) >>> iterableObj range(0, 3) >>> iteratorObj = iter(iterableObj) >>> i = next(iteratorObj) >>> print(i) # body of the for loop 0 >>> i = next(iteratorObj) >>> print(i) # body of the for loop 1 >>> i = next(iteratorObj) >>> print(i) # body of the for loop 2 >>> i = next(iteratorObj) Traceback (most recent call last): File "<stdin>", line 1, in <module> 1 StopIteration لو استدعينا الدالة ()next بعد العنصر الأخير من الكائن القابل للتكرار المعاد، سيعرض بايثون استثناء وقف التكرار StopIteration المشار إليه بالرقم 1؛ بينما في حلقات for التكرارية وبدلًا من جعل البرنامج يتوقف، تستخدم هذا الاستثناء لمعرفة متى يجب وقف التكرار. ويمكن للمكرِّر أن يمر على العناصر في الكائن القابل للتكرار مرةً واحدةً فقط، بما يشبه إمكانية استخدام الدالتين ()open و ()readlines لقراءة محتويات ملف ما لمرة واحدة فقط قبل أن تضطر إلى إعادة فتح الملف لقراءة محتوياته مجددًا، وفي حال رغبتك بالمرور على الكائن القابل للتكرار مجددًا، فلا بد من استدعاء الدالة ()iter مجددًا لتُنشئ كائنًا مكرِّرًا آخر. يمكنك إنشاء القدر الذي تريد من الكائنات المكرِّرة، إذ سيتتبع كل منها على حدى العنصر التالي الواجب إعادته. لنكتب ما يلي في الصدفة التفاعلية للاطلاع على كيفية العمل: >>> iterableObj = list('cat') >>> iterableObj ['c', 'a', 't'] >>> iteratorObj1 = iter(iterableObj) >>> iteratorObj2 = iter(iterableObj) >>> next(iteratorObj1) 'c' >>> next(iteratorObj1) 'a' >>> next(iteratorObj2) 'c' لا تنسَ أن الكائنات المكرَّرة تُمرَّر مثل وسيط إلى الدالة ()iter، إذ أن الكائن المُعاد عن استدعاءات الدالة ()iter هو كائن مكرِّر. لتمرر الكائنات المكرِّرة إلى الدالة ()next. وفي حال إنشاء أنماط بيانات خاصة باستخدام تعليمات الصنف class، فعندها من الممكن تطبيق التوابع الخاصة ()__iter__ و ()__next__ للتمكن من استخدام الكائنات الخاصة في حلقات for. أخطاء الصيغة وأخطاء زمن التنفيذ والأخطاء الدلالية توجد العديد من طرق تصنيف الأخطاء، إلا أن التصنيف عالي المستوى للأخطاء البرمجية يقسمها إلى ثلاثة أنواع: أخطاء الصياغة Syntax وأخطاء زمن التنفيذ Runtime والأخطاء الدلالية Semantic. يُقصد بالصياغة مجموعة القواعد اللازمة لكتابة أوامر صحيحة في لغة برمجة ما؛ إذ يتسبب خطأ الصياغة مثل إغفال أحد الأقواس أو استخدام نقطة بدلًا من الفاصلة أو غيرها من الأخطاء الإملائية بظهور خطأ صياغة SyntaxError في بايثون فورًا. تُعرف أخطاء الصياغة أيضًا باسم أخطاء التحليل parsing errors، والتي تحدث نتيجة عجز مفسر بايثون عن تحليل النص في الشيفرة المركزية إلى أوامر صحيحة. ولو شبهنا الأمر للغة العربية، فهذا الخطأ يكافئ حالات الأخطاء القواعدية أو حالة جملة من كلمات غير مترابطة لا معنى لها، فالحواسيب تتطلب وجود أوامر محددة ولا يمكنها قراءة أفكار المبرمج لمعرفة ما المتوقع من البرنامج فعله، وبالتالي لن يُشغّل البرنامج بوجود أخطاء صياغة. يحدث خطأ زمن التنفيذ عندما يعجز برنامج ما قيد التشغيل عن أداء مهمة ما، كأن يحاول فتح ملف غير موجود أصلًا، أو أن يقسّم عددًا على الصفر. لو شبهنا الأمر للغة العربية، فهذا الخطأ يكافئ إعطاء أمر مستحيل مثل "ارسم مربعًا باستخدام ثلاثة أضلاع"، فإذا لم يجري التعامل مع خطأ زمن التنفيذ، سيتوقف البرنامج عارضًا رسالة تتبع للخطأ. يمكن اكتشاف أخطاء زمن التنفيذ باستخدام تعليمات try-except التي تشغّل الشيفرة المسؤولة عن معالجة الخطأ. على سبيل المثال، لنكتب الشيفرات التالية في الصدفة التفاعلية: >>> slices = 8 >>> eaters = 0 >>> print('Each person eats', slices / eaters, 'slices.') ستعرض الشيفرة السابقة تتبع الخطأ التالي عند تشغيلها: Traceback (most recent call last): File "<pyshell#4>", line 1, in <module> print('Each person eats', slices / eaters, 'slices.') ZeroDivisionError: division by zero من المهم تذكّر أن رقم السطر الذي يشير إليه متتبع الأخطاء هو السطر الذي اكتشف فيه مفسر بايثون وقوع الخطأ، في حين أن مُسبب الخطأ الحقيقي قد يكون في السطر السابق من الشيفرة أو قبله حتى. يكتشف مفسر بايثون أخطاء الصياغة في الشيفرة المصدرية قبل تشغيل البرنامج حتى، إلا أنها واردة الحدوث خلال التنفيذ، إذ يمكن على سبيل المثال، أن نمرر للدالة ()eval سلسلةً نصيةً مؤلفةً من شيفرات بايثون لتشغلها عند استدعائها، والتي قد تسبب خطأ في الصياغة أثناء التنفيذ. فمثلاً التعليمة ('(eval('print("Hello, world ينقصها علامة الاقتباس المزدوجة النهائية، الأمر الذي لن يكتشفه المفسر إلا عند استدعاء الدالة ()eval. الأخطاء الدلالية (والتي تدعى أيضاَ بالأخطاء المنطقية) فهي أخطاء أدق، فهي لن تتسبب بظهور رسائل أخطاء أو حتى توقف تنفيذ البرنامج، لكنها تجعل الحاسوب ينفذ الأوامر بطريقة مختلفة عن تلك المقصودة من المبرمج، ولو شبهنا الأمر للغة العربية، فهذا الخطأ يكافئ إعلام الحاسوب بشراء علبة حليب، وفي حال وجود بيض في المتجر، أن يشتري دزينة (اثنتا عشر علبة). سيشتري الحاسب في هذه الحالة ثلاثة عشر علبة حليب لأن المتجر لديه بيض فعلًا؛ فالحواسيب تنفذ ما نخبرها به حرفيًا شئنا أم أبينا. لنكتب المثال التالي في الصدفة التفاعلية: >>> print('The sum of 4 and 2 is', '4' + '2') فنحصل على الخرج التالي: The sum of 4 and 2 is 42 من الواضح أن القيمة 42 ليست الجواب الصحيح لعملية جمع العددين 4 و 2، ومع ذلك لم يتوقف البرنامج، إذ يجمع عامل الجمع + في بايثون الأعداد الصحيحة في حين أنه يربط قيم السلاسل النصية في سلسلة واحدة جديدة، وبالتالي حصلنا على جواب خاطئ بسبب استخدام كل من 4 و 2 مثل سلاسل نصية وليس أعداد صحيحة ما سبب الحصول على هذه النتيجة غير المرغوبة. المعاملات Parameters والوسطاء Arguments تُعرّف المعاملات بأنها أسماء المتغيرات الموجودة بين قوسين ضمن تعليمة التصريح عن الدوال def، أما الوسطاء فهي القيم الممررة لدى استدعاء الدوال، والتي تُسند لاحقًا إلى المعاملات. فعلى سبيل المثال، لنكتب ما يلي في الصدفة التفاعلية: 1 >>> def greeting(name, species): ... print(name + ' is a ' + species) ... 2 >>> greeting('Zophie', 'cat') Zophie is a cat كل من name و species في تعليمة def السابقة في السطر رقم 1 هي معاملات، أما 'Zophie' و 'cat' في استدعاء الدالة في السطر رقم 2 فهي وسطاء. يحدث خلط بين هذين المصطلحين عادةً، ولعل من الجيد تذكر أن المعاملات والوسطاء ما هي سوى أسماء أخرى للمتغيرات والقيم على التوالي عند استخدامها في هذا السياق آنف الذكر. تحويل نوع البيانات الضمني Type Coercion والصريح Type Casting يمكن تحويل نوع كائن ما ليصبح كائنًا من نوع آخر. فعلى سبيل المثال، تحوّل التعليمة ('int('42 السلسلة النصية '42' إلى الرقم الصحيح 42. في الواقع ليس الأمر تمامًا عملية تحويل، إذ تُنشئ الدالة ()int كائن عدد صحيح جديد اعتمادًا على الكائن الأصلي؛ فعندما نحوّل نوع البيانات صراحةً، يكون هذا تحويل نوع صريح، رغم أن المبرمجين في الواقع يشيرون إلى هذه العملية بمصطلح تحويل الكائن. تجري بايثون غالبًا تحويلًا ضمنيًا للنوع، كما في حالة تقييم التعبير البرمجي 3.0+2 إلى القيمة 5.0، إذ تُجبر القيمتان 2 و 5.0 على نوع شائع من أنواع البيانات والذي يتمكن المفسر من التعامل معه، ويُدعى هذا التحويل بالتحويل الضمني. قد يؤدي التحويل الضمني أحيانًا إلى نتائج غير متوقعة، إذ يمكن مثلًا أن تُحوّل القيم المنطقية True و False ضمنيًا إلى القيم العددية الصحيحة 1 و 0 على التوالي، رغم أنه من المستحيل أن نعبّر عن القيم المنطقية في الشيفرات الواقعية مستخدمين تلك القيم العددية؛ ما يعني أن التعبير True + False + True يكافئ التعبير 1 + 0 + 1 وسيُقيّم إلى 2. بعد معرفتك لهذا الأمر قد تعتقد أن تمرير قائمة من القيم المنطقية إلى التابع ()sum يمثّل طريقةً جيدةً في معرفة عدد القيم True في تلك القائمة، إلا أن استخدام التابع ()count لهذا الغرض أسرع. الخاصيات Properties والسمات Attributes يُستخدم مصطلحي الخاصيات والسمات في العديد من لغات البرمجة على أنهما كلمتان مترادفتان، لكن لهذين المصطلحين معانٍ مختلفة عن بعضهما في بايثون. فالسمة هي اسم مرتبط بكائن، وتتضمن المتغيرات والتوابع المرتبطة بالكائن. تمتلك لغات البرمجة الأخرى مثل جافا تمتلك توابع getter (الذي تعيد القيمة الموافقة لاسم متغير) و setter (الذي تأخذ معاملًا لتُسنده إلى اسم المتغير) للأصناف، فبدلًا من القدرة على إسناد قيمة إلى السمة مباشرةً، يجب على المبرمج استدعاء التابع setter لهذه السمة، بحيث تضمن الشيفرة الموجودة ضمن التابع setter أن قيمةً مناسبةً للمتغير الخاص بالكائن قد أُسندت إليه؛ بينما يقرأ التابع getter قيمة السمة. لو كانت السمة باسم accountBalance على سبيل المثال، فسيُسمى كلًا من التابعين getter و setter بالشكل ()setAccountBalance و ()getAccountBalance على التوالي. أما في بايثون، فتتيح الخاصيات للبرامج استخدام كل من getters و setters بصياغة أوضح. الشيفرة الثنائية Bytecode والشيفرة التنفيذية للتعليمات Machine Code تُترجم الشيفرة المصدرية إلى شكلٍ من التعليمات يسمى الشيفرة التنفيذية للتعليمات، والتي تتمكن وحدة المعالجة المركزية من تنفيذها مباشرةً، وتتكون هذه الشيفرة من تعليماتٍ مختارة من مجموعة تعليمات وحدة المعالجة المركزية وهي ومجموعة الأوامر المبنية مسبقًا في الحاسوب، ويسمى البرنامج المترجَم والمكون من شيفرة تنفيذية للتعليمات بالثنائي Binary. تمتلك لغة البرمجة سي C برنامج مترجم قادر على ترجمة شيفرات سي المصدرية إلى ثنائي لمعظم وحدات المعالجة المركزية المتوفرة، أما في حال تشغيل لغة برمجة مثل بايثون على نفس مجموعة وحدات المعالجة المركزية، فلا بُد من إنجاز كم هائل من العمل لكتابة مترجمات بايثون مخصصة لكل منها. توجد طريقة أخرى لتحويل الشيفرات المصدرية إلى شيفرات تنفيذية للتعليمات (قابلة للتنفيذ من قبل الحاسب)؛ فبدلًا من إنشاء شيفرة تنفيذية للتعليمات لتُنفّذ مباشرةً من قبل وحدة المعالجة المركزية، يمكن إنشاء شيفرة ثنائية والتي تسمى أيضًا بالشيفرة المحمولة أو p-code، والتي تُنفّذ من قبل مفسر بدلًا من وحدة المعالجة المركزية مباشرةً. تتكون هذه الشيفرة من تعليمات مختارة من مجموعة تعليمات، ورغم كون هذه التعليمات غير قابلة للتنفيذ من قبل وحدات المعالجة المركزية، إلا أن المفسر ينفذ هذه التعليمات. تُخزّن شيفرات بايثون الثنائية ضمن ملفات بالامتداد "pyc."، وقد تلاحظها جنبًا إلى جنب مع ملفاتك ذات الامتداد "py." المتضمنة للشيفرات المصدرية. المفسر CPython المكتوب بلغة سي قادر على ترجمة شيفرة بايثون المصدرية إلى شيفرة بايثون ثنائية لينفّذ لاحقًا التعليمات. ينطبق الأمر ذاته على برنامج Java Virtual Machine -أو اختصارًا JVM- في لغة جافا، والذي ينفذ شيفرات جافا الثنائية. بما أن المفسر CPython مكتوب بلغة سي، فإنه يمتلك مفسر بايثون، وهو قادر على ترجمة الشيفرات لتناسب أي وحدة معالجة مركزية ذات مفسر خاص بها بلغة سي. السكريبت Script والبرنامج Program، لغات كتابة السكريبت ولغات البرمجة الفرق بين السكريبت والبرنامج أو حتى ما بين لغات كتابة السكريبت ولغات البرمجة غامض، فمن المنطقي القول بأن كل سكريبت هو برنامج، وأن كل لغات كتابة السكريبت هي لغات برمجة، لكن تُعد لغات كتابة السكريبت أسهل أحيانًا، أو أنها ليست لغات برمجة حقيقية. إحدى طرق تمييز السكريبت عن البرنامج هي طريقة تنفيذ الشيفرة. إذ يُفسّر السكريبت المكتوب بلغة كتابة السكريبت مباشرةً من الشيفرة المصدرية، في حين تًترجم البرامج المكتوبة بلغات البرمجة إلى شيفرات ثنائية، لكن غالبًا ما يُنظر إلى بايثون على أنها لغة كتابة سكريبت رغم وجود خطوة الترجمة إلى شيفرة ثنائية لدى تشغيل برامج بايثون. لا تعد لغة جافا مثل لغة كتابة سكريبت رغم أنها تُنشئ شيفرات ثنائية تمامًا مثل بايثون. من الناحية التقنية: لن تُترجم اللغات أو تُفسّر ما لم يوجد مترجم أو مفسر لهذه اللغة، ومن الممكن إنشاء مترجم أو مفسر لأي لغة. يمكن في الواقع مناقشة الاختلافات، إلا أنها ليست بتلك الأهمية، فلغات كتابة السكريبت ليست بالضرورة أقل قدرة ولا لغات البرمجة بالضرورة أصعب للتعامل معها. المكتبة Library وإطار العمل Framework وحزمة أدوات تطوير البرمجيات SDK والمحرك Engine وواجهة برمجة التطبيقات API قد يوفر استخدام شيفرات الآخرين الكثير من الوقت، وغالبًا ما ستجد هذه الشيفرات ضمن مكتبات، أو أطر عمل، أو حزم أدوات تطوير، أو محركات، أو واجهات تطوير تطبيقات، والفرق ما بين الكيانات السابقة دقيق ومهم. المكتبة هي مصطلح عام يدل على مجموعة من الشيفرات المُعدّة من قبل جهة خارجية، وقد تتضمن دوالًا وأصنافًا وغيرها من أجزاء الشيفرات ليستخدمها المطور، وتكون عادةً مكتبات بايثون على هيئة حزم، أو مجرد وحدة واحدة. تكون المكتبات غالبًا مخصصةً للغة برمجة معينة، ولا يتعين على المبرمج معرفة آلية عمل شيفرة المكتبة، فكل ما عليه معرفته هو كيفية استدعاء والتخاطب مع الشيفرة الموجودة في المكتبة، أما المكتبة المعيارية، مثل مكتبة بايثون المعيارية، فهي مكتبة شيفرات يُفترض توفرها لكافة تطبيقات لغة البرمجة. إطار العمل هو مجموعة من الشيفرات عكسية التحكم inversion of control، بمعنى أن المطور يُنشئ دوالًا ليستدعيها إطار العمل عند الحاجة، على عكس استدعاء الدوال في شيفرات المطور. يُشبّه عادة مصطلح اللاتحكمية بعبارة "لا تتصل بنا، سنتصل نحن بك". على سبيل المثال، تتضمن كتابة شيفرة لإطار عمل تطبيق ويب إنشاء دوال لصفحات الويب التي سيستدعيها إطار العمل لدى وصول طلب ويب. تتضمن حزمة أدوات التطوير software development kit- أو اختصارًا SDK- مكتبات شيفرات وتوثيقات وأدوات برمجية للمساعدة في في إنشاء تطبيقات عاملة على أنظمة تشغيل أو منصات معينة. على سبيل المثال، تُستخدم كل من Android SDK و iOS SDK لإنشاء تطبيقات الهاتف المحمول لأنظمة أندرويد و iOS على التوالي، كما أن حزمة تطوير جافا Java Development Kit-أو اختصارًا JDK- ما هي إلا SDK مُعدّة لإنشاء التطبيقات العاملة على JVM. المحرك هو نظام ضخم مستقل يمكن التحكم به خارجيًا من قبل برامج المطور، ويستدعي المطورون عادةً الدوال الموجودة في محرك ما لإنجاز مهام كبيرة ومعقدة، ومن الأمثلة على المحركات: محركات الألعاب والمحركات الفيزيائية ومحركات التوصية ومحركات قواعد البيانات ومحركات الشطرنج ومحركات البحث. واجهة برمجة التطبيقات Application Programming Interface -أو اختصارًا API- هي الواجهة العمومية للمكتبة، أو حزمة أدوات التطوير، أو إطار العمل، أو المحرك؛ إذ أنها تحدد كيفية استدعاء الدوال أو إنشاء طلبات الوصول إلى موارد المكتبة. يقع على عاتق مُنشئي المكتبات إعداد التوثيقات حول واجهة برمجة التطبيقات المتوفرة، وتنشئ العديد من الشبكات الاجتماعية الشائعة والمواقع الإلكترونية واجهة برمجة التطبيقات من نوع HTTP API، التي تتيح للبرامج الوصول إلى خدماتها ذاتيًا، إذ يتيح لك استخدام واجهات برمجة التطبيقات هذه كتابة برامج قادرة مثلًا على النشر ذاتيًا على فيسبوك أو قراءة الأخبار عبر تويتر. الخلاصة من السهل أن تمارس البرمجة لسنوات مع بقاء بعض المصطلحات البرمجية تثير الالتباس بالنسبة لك، وبما أن معظم التطبيقات البرمجية تُنشئ من قبل فريق كامل من المطورين وليس من قبل أفراد، فلا بُد من فهم المصطلحات بدقة والتمييز بين المتقارب منها. لبعض المصطلحات مثل القيم والمتغيرات والدوال أسماء مختلفة باختلاف السياق، مثل العناصر والمعاملات والوسطاء والتوابع، ومن السهل الخلط بين العديد من المصطلحات، إذ لا يعد الخلط بين بعض المصطلحات في الحياة اليومية أمرًا كارثيًا، مثل الخلط بين الخاصية والسمة أو الكتلة والمتن أو الاستثناء والخطأ، أو حتى الاختلافات الدقيقة ما بين المكتبة وإطار العمل وحزمة أدوات التطوير والمحرك وواجهة برمجة التطبيقات، فلا يؤدي فهمها الخاطئ بالضرورة إلى عدم عمل شيفرتك، إلا أنك ستظهر بمظهر غير المحترف، فعلى سبيل المثال، يخلط المبتدئون ما بين مفاهيم من قبيل التعابير والتعليمات البرمجية أو الدوال والتوابع أو المعاملات والوسطاء، إلا أن مصطلحاتٍ أخرى مثل قابل للتكرار والمكرِّر أو خطأ الصياغة والخطأ الدلالي أو الشيفرة الثنائية والشيفرة التنفيذية للتعليمات، فلكل منها معناها الخاص الذي عليك فهمه بعمق وعدم الخلط بينه وبين مقابله مالم ترغب بإثارة الاستغراب لدى زملاء عملك. قدمنا في هذا المقال مجموعة من المصطلحات التي قد تخلط بينها، ومع ذلك ستجد دومًا أن استخدام المصطلحات يختلف من لغة لأخرى وحتى من مبرمج لآخر، وستغدو معتادًا أكثر على المصطلحات مع الخبرة والبحوثات المتكررة على الإنترنت. ترجمة -وبتصرف- لجزء من الفصل السابع "المصطلحات البرمجية" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: مصطلحات بايثون البرمجية المرجع الشامل إلى تعلم لغة بايثون التعامل مع الملفات والمسارات في بايثون اختيار أسماء برمجية مفهومة في بايثون النسخة العربية الكاملة لكتاب: البرمجة بلغة بايثون
-
أنشأ راندال مونرو Randall Munroe في مدونة الكاريكاتير XKCD مخططًا تقنيًا لصاروخ "زحل الخامس" تحت عنوان "Up Goer Five" مُستخدمًا فقط الكلمات الإنجليزية الألف الأشهر، إذ بسّط المصطلحات التقنية إلى جمل يستطيع أي طفل فهمها، إلا أن هذا الأمر يسلط الضوء أيضًا على تفسير عدم إمكانية شرح أي شيء باستخدام مصطلحات بسيطة؛ فالشرح القائل "شيء يساعد الناس على الهرب بسرعة كبيرة في حال وجود مشكلة واشتعال كل شيء ما يجعلهم يقررون عدم الذهاب إلى الفضاء" أسهل فهمًا للمتلقي العادي من عبارة "بدء نظام الهروب Launch Escape System". الشرح الأول مبالغ بإطالته ليستخدمه مهندسو الإدارة الوطنية للملاحة الجوية والفضاء NASA في عملهم اليومي، بل أنهم حتى قد يفضلون استخدام الاختصار LES. قد تسبب مصطلحات الحوسبة الحيرة والقلق للمبرمجين المبتدئين، إلا أنها أمر لا بد من تعلمه، إذ تختلف العديد من المصطلحات في بايثون ومجال التطوير البرمجي عمومًا اختلافات دقيقة، لدرجة أن المطورين المتمرسين قد يخلطون بينها دون اكتراث. قد يختلف التعريف التقني لهذه المصطلحات من لغة برمجة لأخرى، إلا أن هذا المقال يغطي المصطلحات المتعلقة بلغة بايثون تحديدًا، إذ ستحصل فيه على فهم واسع لمفاهيم لغة البرمجة الكامنة في الكواليس حتى لو لم يكن عميقًا. تعاريف ما أن يبلغ عدد المبرمجين في نفس المكان اثنين، حتى تصبح احتمالية انطلاق نقاش حول دلالات اللغة 100%، فاللغة سهلة والبشر هم سادات الكلمات وليس العكس. قد يستخدم بعض المطورين المصطلحات بطريقة مختلفة قليلًا، ومع ذلك يبقى التعرف على هذه المصطلحات أمرًا مفيدًا. سنستعرض في هذا المقال قسمًا من هذه المصطلحات وكيفية مقارنتها ببعضها بعضًا، وفي حال رغبتك بالحصول على قائمة مرتبة أبجديًا بالمصطلحات، فيمكنك الاعتماد على تلك الرسمية الخاصة ببايثون وصولًا إلى التعريفات الأساسية. مما لا شك فيه أن بعض المبرمجين سيقرؤون التعريفات الواردة في هذا المقال طارحين حالاتٍ خاصة أو استثناءات وهي في الواقع غير محدودة، فلا يمكن عد هذا المقال دليلًا إرشاديًا كاملًا ونهائيًا، بل إنه يهدف إلى تزويدك بالتعريفات حتى لو لم تكن شاملة تمامًا، فكما هو الحال مع كل ميادين البرمجة، يوجد دائمًا المزيد لتعلمه. لغة بايثون ومفسر بايثون تنطوي كلمة "بايثون" على معانٍ عديدة، إذ سُميّت لغة البرمجة بايثون نسبةً إلى الفرقة الكوميدية البريطانية Monty Python، وليس نسبةً إلى الثعبان كما توحي الترجمة الحرفية، رغم كون مصادر بايثون التعليمية وتوثيقاتها تشير إلى كلًا من فرقة Monty Python والثعبان مثل مرجع لأصل التسمية، وكذلك الأمر من حيث الشق البرمجي، إذ تحمل كلمة بايثون معنيين أيضًا. يمكن أن نقول "تُشغّل بايثون برنامجًا" أو "ستعرض بايثون استثناءً"، والمقصود هنا هو مفسر بايثون -وهو البرنامج الفعلي المسؤول عن قراءة النصوص ضمن الملفات ذات اللاحقة "py." مُنفذًا التعليمات الواردة فيها؛ فعندما نقول "مفسر بايثون"، نقصد غالبًا "CPython"، وهو المفسر المُعتمد من قبل مؤسسة بايثون للبرمجيات Python Software Foundation. إذًا، CPython هو تنفيذ للغة بايثون، بمعنى أنه برمجية أُنشئت لتحقق مواصفات محددة، ولكن يوجد برمجيات غيرها؛ ففي حين أن المفسر CPython مكتوب بلغة البرمجة سي C، المفسر Jython مكتوبٌ بلغة جافا Java بغية تشغيل نصوص بايثون البرمجية القابلة للتشغيل المتبادل مع برامج جافا؛ أما PyPy فهو مترجم ديناميكي just-in-time compiler لبايثون والذي يترجم البرامج لدى تنفيذها، فهو مكتوبٌ بلغة بايثون. تُشغّل كل من هذه التنفيذات الشبفرات المصدرية المكتوبة بلغة البرمجة بايثون، وهي المقصودة لدى قولنا: "هذا برنامج بايثون" أو "أتعلم بايثون".ومن الناحية المثالية فإن أي مفسر بايثون قادر على تشغيل أي شيفرة مصدرية مكتوبة بلغة بايثون، أما في الواقع العملي فتوجد بعض حالات عدم التوافق البسيطة والاختلافات ما بين المفسرات، ولعل سبب تسمية CPython بالمفسر المرجعي للغة بايثون هو أنه في حال وجود اختلافات في كيفية تفسير شيفرة بايثون ما بين CPython وغيره من المفسرات، سيكون ما فسره CPython هو الصحيح والمُتفق عليه. كنس المهملات Garbage Collection كان على المبرمج سابقًا في لغات البرمجة القديمة توجيه البرنامج لتخصيص أو إلغاء تخصيص أو تحرير الذاكرة لبنى المعطيات حسب الحاجة، وقد كانت عملية تخصيص الذاكرة اليدوية هذه مصدرًا للعديد من الأخطاء، مثل تسريبات الذاكرة memory leaks (التي تحدث عند نسيان المبرمج لتحرير الذاكرة) أو أخطاء التحرير المزدوج للذاكرة double-free bugs (إذ يحرر المبرمج نفس الجزء من الذاكرة مرتين، متسببًا في تلف البيانات). تمنلك بايثون ميزة تجميع وكنس المهملات لتجنب هذه الأخطاء، والتي تعد أحد أشكال الإدارة التلقائية للذاكرة والتي تتعقّب التوقيت المناسب لتخصيص وتحرير الذاكرة نيابةً عن المبرمج. من الممكن النظر إلى تجميع المهملات مثل عملية إعادة تدوير للذاكرة، لكونها تتيح الذاكرة للبيانات الجديدة. فعلى سبيل المثال، لنكتب ما يلي في الصدفة التفاعلية: >>> def someFunction(): ... print('someFunction() called.') ... spam = ['cat', 'dog', 'moose'] ... >>> someFunction() someFunction() called. تخصص بايثون لدى استدعاء الدالة ()someFunction الذاكرة للقائمة ['cat', 'dog', 'moose']. وبالتالي ما من حاجة لمعرفة المبرمج عدد البايتات من الذاكرة المتوجب طلبها لأن بايثون تدير هذا الأمر نلقائيًا، إذ سيحرر مُجمّع المهملات المتغيرات المحلية بمجرد إعادة الدالة المُستدعاة، ما يجعل الذاكرة متاحةً لبيانات جديدة. إذًا، يجعل مفهوم تجميع المهملات من البرمجة أمرًا أسهل وأقل عرضة للأخطاء. القيم المجردة Literals القيمة المجردة هي نص ضمن الشيفرة المصدرية ذات قيمة ثابتة كما هي مكتوبة. ففي المثال التالي 42 هو قيمة صحيحة مجردة و 'Zophie' هي سلسلة نصية مجردة.: >>> age = 42 + len('Zophie') يمكن فهم القيم المجردة مثل قيم تظهر كما هي بحرفيتها مثل نص ضمن الشيفرة المصدرية، ولا يمكن إلا لأنواع البيانات المُعرّفة أصلًا في بايثون امتلاك قيم مجردة في الشيفرة المصدرية، وبالتالي ليس المتغير age قيمة مجردة. يبيّن الجدول 1 التالي بعض الأمثلة عن القيم المجردة في بايثون. القيمة المجردة نوع البيانات 42 عدد صحيح 3.14 عدد عشري 1.4886191506362924e+36 عدد عشري """!Howdy""" سلسلة نصية 'r'Green\Blue سلسلة نصية [] قائمة {'name': 'Zophie'} قاموس 'b'\x41 بايتات True بولياني None نوع فارغ NoneType قد يجادل بعض المعترضين في أن بعضًا من القيم لا يمثّل قيمًا مجردة استنادًا إلى التوثيق الرسمي للغة بايثون، فمن الناحية التقنية، 5- ليست قيمة مجردة في بايثون لأن اللغة تعرّف رمز السالب (-) مثل عامل على القيمة المجردة 5، كما تُعد القيم True و False و None كلمات مفتاحية في بايثون وليست قيمًا مجردة، في حين أن [] و{} تسمى مُخرجات أو ذرات atoms اعتمادًا على جزء التوثيق الرسمي الذي تبحث ضمنه. بغض النظر عن ذلك، القيمة المجردة هي مصطلح شائع يستخدمه محترفو البرمجيات للدلالة على كل الأمثلة السابقة. الكلمات المفتاحية Keywords تمتلك كل لغة برمجة الكلمات المفتاحية الخاصة بها؛ وكلمات بايثون المفتاحية هي مجموعة من الأسماء المحجوزة للاستخدام مثل جزء من بناء اللغة والتي لا يمكن استخدامها أسماءً للمتغيرات (أي المعرّفات). على سبيل المثال، لا يمكنك تسمية أحد المتغيرات بالاسم while لأنها كلمة محجوزة للاستخدام في حلقات while التكرارية، وفيما يلي الكلمات المفتاحية المحجوزة في بايثون وفقًا للإصدار 3.9 منها. and continue finally is raise as def for lambda return assert del from None True async elif global nonlocal try await else if not while break except import or with class False in pass yield نلاحظ أن كلمات بايثون المفتاحية هي دومًا باللغة الإنجليزية وغير متاحة بأي لغات أخرى. فعلى سبيل المثال، في الدالة التالية المعرفات مكتوبة باللغة الاسبانية، في حين بقيت الكلمتين def و return المفتاحيتين في بايثون بالإنجليزية: def agregarDosNúmeros(primerNúmero, segundoNúmero): return primerNúmero + segundoNúmero تهيمن اللغة الإنجليزية لسوء الحظ على مجال البرمجة بالنسبة للتعداد البالغ 6.5 مليار من غير الناطقين بها. الكائنات Objects والقيم Values والنسخ Instances والهويات Identities يمثّل الكائن جزءًا من البيانات سواء كانت عددًا أو بعضًا من النصوص أو هيكل بيانات أكثر تعقيدًا، مثل القائمة، أو القاموس. ويمكن تخزين الكائنات ضمن متغيرات وتمريرها مثل وسطاء عند استدعاء الدوال واستخدامها مثل قيمة معادة عن استدعاء الدوال. تمتلك جميع الكائنات قيمةً وهوية ونوع بيانات، والقيمة هي البيانات التي يمثلها الكائن، مثل العدد الصحيح 42، أو السلسلة النصية 'hello'. يستخدم بعض المبرمجين في الواقع مصطلح القيمة بمثابة مرادف لمصطلح الكائن، لا سيما لأنواع البيانات البسيطة مثل الأعداد الصحيحة أو السلاسل النصية، رغم أن هذا الأمر قد يسبب الإزعاج. يُنشَأ الكائن بهوية، وهي رقم صحيح فريد من الممكن الإطلاع عليه باستدعاء الدالة ()id. على سبيل المثال، لنكتب ما يلي في الصدفة التفاعلية: >>> spam = ['cat', 'dog', 'moose'] >>> id(spam) 33805656 يُخزّن المتغير spam كائنًا من نوع البيانات "قائمة"، قيمته تساوي ['cat', 'dog', 'moose'] أما هويته فهي 33805656، رغم كون العدد الصحيح ID هذا يتغير في كل مرة يُنفّذ فيها البرنامج، وبالتالي ستكون القيمة على حاسوبك مختلفة عن هذه المعروضة هنا، ولكن بمجرد إنشاء الكائن، لن تتغير قيمة هويته طالما أن البرنامج قيد التشغيل، ورغم أن كلًا من هوية ونوع بيانات الكائن لا يتغيران أثناء تشغيل البرنامج، إلا أن قيمته قد تتغير، كما في المثال التالي: >>> spam.append('snake') >>> spam ['cat', 'dog', 'moose', 'snake'] >>> id(spam) 33805656 وبذلك أصبحت القائمة تتضمّن أيضًا العنصر 'snake'، ولكن وكما هو مبين من نتيجة استدعاء الدالة (id(spam أن هويتها لم تتغير وبقيت نفسها، ولكن ماذا لو كتبنا الشيفرات التالية: >>> spam = [1, 2, 3] >>> id(spam) 33838544 استُبدلت القيمة في المتغير spam بكائن قائمة جديد وبهوية جديدة قيمتها 33838544 بدلًا عن 33805656، إذ يختلف المعرف spam عن مفهوم الهوية من حيث أن عدّة معرفات قد تتبع لكائن واحد، كما في المثال التالي الذي أسندنا فيه متغيرين لنفس القاموس: >>> spam = {'name': 'Zophie'} >>> id(spam) 33861824 >>> eggs = spam >>> id(eggs) 33861824 ومنه نجد أن هوية كل من المعرفين spam و eggs نفسها وتساوي 33861824، لأنهما يتبعان لنفس كائن القاموس. الآن وبتغيير قيمة spam في الصدفة التفاعلية: >>> spam = {'name': 'Zophie'} >>> eggs = spam 1 >>> spam['name'] = 'Al' >>> spam {'name': 'Al'} >>> eggs 2 {'name': 'Al'} ومنه نجد أن التغييرات على المتغير spam في السطر 1 قد ظهرت أيضًا على المتغير eggs في السطر 2، وسبب ذلك أن كلاهما يتبع لنفس الكائن. مفهوم المتغيرات المجرد: الصندوق BOX مقابل العنوان LABEL تستخدم الكثير من الكتب التمهيدية مصطلح "الصناديق" بمثابة مفهوم مجرد للمتغيرات، ويُعد الأمر بسيطًا جدًا، فمن السهل فهم المتغيرات على أنها صناديق تُخزَّن فيها القيم كما في الشكل 1، إلا أن هذا المفهوم يتهاوى لدى التفكير من وجهة النظر المرجعية، فمثلًا المتغيرين spam و eggs السابقين لا يخزنان قاموسين منفصلين (نسختين من نفس القاموس) بل يخزنان مرجعًا في ذاكرة الحاسوب لنفس القاموس. شكل 1: تفيد العديد من المراجع بأنه من الممكن فهم المتغيرات على أنها صناديق تحتوي على قيم المتغيرات في بايثون من الناحية التقنية هي مراجع وليست حاويات containers للقيم، وذلك بغض النظر عن نوع البيانات التي تحتويها. رغم بساطة مفهوم الصندوق إلا أنه منقوص، فبدلًا من فهم المتغيرات على أنها صناديق، يُفضّل فهمها مثل عناوين للكائنات في الذاكرة، والشكل 2 التالي يبين العناوين لأمثلة المتغيرين spam و eggs السابقين. شكل 2: من الممكن أيضًا فهم المتغيرات مثل عناوين للقيم قد تشير عدة متغيرات إلى نفس الكائن، لذلك قد يُخزَّن الكائن الواحد في عدة متغيرات، ولكن لا يمكن للصناديق المختلفة أن تخزّن نفس الكائن الوحيد، ما يجعل فهم المتغيرات مثل عناوين أسهل وأقرب للفهم. قد تكون معرضًا لوقوع أخطاء في شيفرتك ما لم تدرك أن عامل الإسناد = ينسخ دومًا مرجع الكائن وليس الكائن نفسه، وذلك بظنك أنك تُنشئ نسختين متطابقتين من الكائن ولكن في الواقع أنت تنسخ مرجع الكائن الأصلي. لحسن الحظ أن هذه المسألة لا تتعلق بالقيم الثابتة immutable من أعداد صحيحة وسلاسل نصية وصفوف، وذلك لسبب سنشرحه لاحقًا في هذا الكتاب ضمن فقرة "متغيرات وثوابت". يمكن استخدام العامل is لمقارنة فيما إذا كان لكائنين نفس الهوية، وبالمقابل يمكن استخدام العامل == للتحقق فقط من كون قيمتي الكائنين متطابقتين، وبالتالي التعبير x is y مكافئ لنظيره (id(x) == id(y. لنكتب ما يلي في الصدفة التفاعلية لنلاحظ الفرق: >>> spam = {'name': 'Zophie'} 1 >>> eggs = spam >>> spam is eggs True >>> spam == eggs True 2 >>> fish = {'name': 'Zophie'} >>> spam == fish True >>> spam is fish False إذ يشير المتغيران spam وeggs إلى نفس كائن القاموس (في السطر 1)، وبالتالي فهما متساويان بالقيمة والهوية.، بينما يشير المتغير fish إلى كائن قاموس آخر (في السطر 2)، رغم كونه يتضمن بيانات مطابقة لتلك الموجودة في المتغيرين spam و eggs. يعني تطابق البيانات أن للمتغير fish نفس القيمة كما في المتغيرين spam و eggs، إلا أنهما كائنان مختلفان بهويتين مختلفتين. العناصر Items ندعو في بايثون الكائن الموجود ضمن كائن حاوية مثل قائمة أو قاموس بالعنصر، فعلى سبيل المثال، السلسة النصية ضمن القائمة ['dog', 'cat', 'moose'] هي كائنات ولكننا ندعوها أيضًا بالعناصر. الثابت Immutable والمتغير Mutable كما لاحظنا سابقًا، تمتلك كل الكائنات في بايثون قيمة ونوع بيانات وهوية، ويمكن منها تغيير القيمة فقط، وبما أننا نستطيع تغيير قيمة الكائن فهو كائن متغيّر؛ أما إذا كانت قيمته غير متغيرة فندعوه بالكائن الثابت. يعرض الجدول 2 قائمةً ببعض أنواع البيانات الثابتة والمتغيرة في بايثون. جدول 2: بعض أنواع البيانات الثابتة والمتغيرة في بايثون أنواع البيانات الثابتة أنواع البيانات المتغيرة عدد صحيح قائمة عدد عشري قاموس قيمة بوليانية مجموعات سلسلة نصية مصفوفة بايت Bytearray مجموعة ثابتة Frozen set مصفوفة بايتات صف Tuple فلو أعدت الكتابة في متغير ما، قد يبدو لك أنك قد غيرت قيمة الكائن الخاص به، كما في المثال التالي في الصدفة التفاعلية: >>> spam = 'hello' >>> spam 'hello' >>> spam = 'goodbye' >>> spam 'goodbye' لكننا لم نغيّر هنا قيمة الكائن 'hello' من 'hello' إلى 'goodbye'، فهما كائنان منفصلان، وكل ما فعلناه في الشيفرة السابقة هو مجرّد جعل المتغير spam يشير إلى الكائن 'goodbye' بدلًا من 'hello'، ويمكن التحقق من صحة أنهما فعلًا كائنين مستقلين باستخدام الدالة ()id للحصول على هوية كل منهما: >>> spam = 'hello' >>> id(spam) 40718944 >>> spam = 'goodbye' >>> id(spam) 40719224 ومنه نجد أنّ لهذين الكائنين النصيين هويتان مختلفتان (40718944 و 40719224) نظرًا لكونهما كائنين مستقلين، بينما يمكن تغيير قيمة المتغيرات التي تشير إلى كائنات متغيرة، مثل القوائم والصفوف مع الحفاظ على نفس الهوية. دعنا نكتب على سبيل المثال الشيفرات التالية في الصدفة التفاعلية: >>> spam = ['cat', 'dog'] >>> id(spam) 33805576 1 >>> spam.append('moose') 2 >>> spam[0] = 'snake' >>> spam ['snake', 'dog', 'moose'] >>> id(spam) 33805576 تعمل كلًا من الدالة ()append (في السطر 1) وعملية الإسناد للعنصر (في السطر 2) على تعديل قيمة القائمة في مكانها (من أجل نفس الكائن بنفس الهوية)؛ فقد تغيرت قيمة القائمة، إلا أن هويتها قد بقيت كما هي 33805576؛ إما في حال استخدام العامل + لبناء تسلسل القائمة، فإننا ننشئ كائنًا جديدًا (ذو هوية جديدة) بديلًا عن ذلك السابق على النحو التالي: >>> spam = spam + ['rat'] >>> spam ['snake', 'dog', 'moose', 'rat'] >>> id(spam) 33840064 ومنه نجد أن بناء تسلسل القائمة وفق الطريق السابقة (باستخدام العامل +) قد أنشأ قائمة جديدة بهوية جديدة، وبمجرد حدوث ذلك يُحرّر مُجمّع المهملات القائمة القديمة من الذاكرة. لمعرفة التوابع والعمليات التي تعدل قيم الكائنات مع الحفاظ على هويتها وتلك التي تغيرها، عليك الرجوع إلى توثيق بايثون، ولكن يمكنك أخذ القاعدة التالية بالحسبان: إذا رأيت قيمةً مجردةً ضمن الشيفرة المصدرية، مثل ['rat'] في المثال السابق، فعندها سينشئ بايثون كائنًا جديدًا بهوية جديدة غالبًا، في حين أن التوابع المُستدعاة مع استخدام الكائن بمثابة وسيط لها، مثل التابع ()append، فغالبًا ما تعدّل قيمة الكائن مُحافظةً على هويته. في حال التعامل مع أنواع البيانات الثابتة مثل الأعداد الصحيحة والسلاسل النصية والصفوف، ستكون عمليات الإسناد هي الأبسط. لنكتب على سبيل المثال ما يلي في الصدفة التفاعلية: >>> fish = 'Goodbye' >>> id(fish) 33827584 1 >>> fish = 'Hello' >>> id(fish) 33863820 2 >>> fish = fish + ', world!' >>> fish 'Hello, world!' >>> id(fish) 33870056 3 >>> fish[0] = 'J' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'str' object does not support item assignment السلاسل النصية ثابتة، بمعني أنه من غير الممكن تغيير قيمة السلسلة نفسها، ففي حين يبدو أننا قد غيرنا قيمة السلسة النصية في المتغير fish من القيمة 'Goodbye' إلى القيمة 'Hello' (في السطر رقم 1)، إلا أنّه في الواقع قد غيّرنا قيمته إلى كائن سلسلة نصية جديدة ذو هوية جديدة؛ وعلى نحوٍ مشابه، ينشئ التعبير البرمجي باستخدام بناء تسلسل القائمة كائن قائمة جديد (كما في السطر 2) ذو هوية جديدة. لا يُسمَح في الإصدار 3 من بايثون تغيير السلاسل النصية باستخدام عمليات الإسناد دون تغيير هوية الكائن. تُعرّف قيمة الصف وفقًا للكائنات التي يحتويها وترتيب هذه الكائنات؛ فالصفوف هي تسلسلات من الكائنات الثابتة التي تحصر القيم في أقواس، ما يعني أنه من غير الممكن تغيير العناصر الموجودة في الصف، كما في المثال التالي: >>> eggs = ('cat', 'dog', [2, 4, 6]) >>> id(eggs) 39560896 >>> id(eggs[2]) 40654152 >>> eggs[2] = eggs[2] + [8, 10] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object does not support item assignment أما في حال تواجد قائمة متغيرة ضمن الصف الثابت، فستبقى متغيّرة دون تغيير هويتها، كما في المثال التالي: >>> eggs[2].append(8) >>> eggs[2].append(10) >>> eggs ('cat', 'dog', [2, 4, 6, 8, 10]) >>> id(eggs) 39560896 >>> id(eggs[2]) 40654152 ورغم كون هذه الحالة خاصة ومبهمة قليلًا، إلا أنه من الضروري أخذها بالحسبان، فلا يزال الصف يشير إلى نفس الكائنات كما هو موضح في الشكل 3، ولكن إذا احتوى الصف على كائن متغيّر وفي حال تغيرت قيمة هذا الكائن -بمعنى أن هذا الكائن قد تغير- تتغيّر قيمة الصف أيضًا. يعد كل محترفي بايثون الصف ثابتًا، ويعتمد أمر تسمية بعض الصفوف بأنها متغيرة على تعريفك وفهمك لها. الشكل 3: رغم كون مجموعة الكائنات ضمن الصف ثابتة، إلا أن المتغيرات نفسها قادرة على التغير. الفهارس Indexes والمفاتيح Keys والقيم المعماة Hashes قوائم وقواميس بايثون هي قيم يمكن أن تحتوي على قيم أخرى متعددة، ويمكن للوصول إلى هذه القيم استخدام عامل الفهرس المكون من زوج من الأقواس المعقوفة وعدد صحيح يسمى الفهرس لتحديد القيمة المراد الوصول إليها، ولنبيّن كيفية استخدام الفهرس مع القائمة، سنكتب ما يلي في الصدفة التفاعلية: >>> spam = ['cat', 'dog', 'moose'] >>> spam[0] 'cat' >>> spam[-2] 'dog' في المثال السابق، الرقم 0 هو فهرس، إذ أن الفهرس الأول هو 0 وليس 1، إذ تستخدم بايثون ومعظم لغات البرمجة الصفر بدايةً للفهارس، أما اللغات التي تعتمد 1 بدايةً للفهارس فهي نادرة وأشهرها لغتي Lua و R، كما أن بايثون تدعم الفهارس السالبة، إذ يشير مثلًا "1-" إلى العنصر الأخير في القائمة، في حين يشير "2-" إلى العنصر ما قبل الأخير، وهكذا. يمكن فهم الفهرس السالب بالشكل [spam[–n مكافئًا لاستخدام [spam[len(spam) – n. ملاحظة: قال عالم الحاسوب والمغني وكاتب الأغاني ستان كيلي-بوتل Stan Kelly-Bootle مرة مازحًا: أيجب أن تبدأ فهارس المصفوفات من الصفر أو الواحد؟ كان اقتراحي بمثابة حلٍ وسطي أن تبدأ من 0.5 ولكنه رفض دون سبب وجيه على ما أعتقد. يمكن استخدام عامل الفهرس على السلسلة المجردة كما هي، رغم كون الأقواس المعقوصة الخاصة بالفهرس قد تبدو غريبة وغير ضرورية في هذه الحالة، كما في المثال: >>> ['cat', 'dog', 'moose'][2] 'moose' : كما يمكن استخدام الفهارس على قيم غير القوائم، مثل السلاسل النصية بغية الحصول على محارف معينة من السلسلة، على النحو التالي: >>> 'Hello, world'[0] 'H' أما قواميس بايثون، فتُرتّب على هيئة أزواج "مفتاح-قيمة"، على النحو التالي: >>> spam = {'name': 'Zophie'} >>> spam['name'] 'Zophie' تنحصر فهارس القوائم بكونها أعداد صحيحة، لكن فهارس قواميس بايثون هي مفاتيح، يمكن أن تكون أي قيمة قابلة للتعمية؛ فالقيمة المعماة هي عدد صحيح شبيه بالبصمة الخاصة بالقيمة، إذ أن القيمة المعماة الخاصة بكائن ما لا تتغير طوال فترة وجود الكائن، وللكائنات المتماثلة في قيمها، قيم معماة متماثلة أيضًا. في مثالنا السابق، السلسلة النصية 'name' هي مفتاح القيمة 'Zophie'. وتعيد الدالة ()hash القيمة المعماة للكائنات القابلة للتعمية. الكائنات الثابتة مثل الأعداد الصحيحة والأعداد العشرية والسلاسل النصية والصفوف قابلة للتعمية، في حين أن القوائم وغيرها من الكائنات المتغيرة غير قابلة للتعمية. لنكتب ما يلي في الصدفة التفاعلية: >>> hash('hello') -1734230105925061914 >>> hash(42) 42 >>> hash(3.14) 322818021289917443 >>> hash((1, 2, 3)) 2528502973977326415 >>> hash([1, 2, 3]) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list' تُستخدم المفاتيح المعماة لإيجاد العناصر المخزنة في القواميس وغيرها من هياكل البيانات، ولهذا لا يمكن استخدام قائمة متغيرة مثل مفتاح لقاموس: >>> d = {} >>> d[[1, 2, 3]] = 'some value' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list' تختلف القيمة المعماة عن الهوية، فلو كان لدينا كائنان مختلفان يتضمنان نفس القيمة، سيكون لكل منهما هويته الخاصة، أما القيمة المعماة فستكون نفسها لكليهما. لنكتب على سبيل المثال ما يلي في الصدفة التفاعلية: >>> a = ('cat', 'dog', 'moose') >>> b = ('cat', 'dog', 'moose') >>> id(a), id(b) (37111992, 37112136) 1 >>> id(a) == id(b) False >>> hash(a), hash(b) (-3478972040190420094, -3478972040190420094) 2 >>> hash(a) == hash(b) True يمتلك الصفان المُشار إليهما بواسطة a و b هويتين مختلفتين كما هو موضح في السطر 1 من الشيفرة السابقة، ولكن كونهما يحتويان على قيمتين متطابقتين فالقيم المعماة لهما متساوية كما هو موضح في السطر 2. يكون الصف قابلًا للتعمية في حال احتوائه على عناصر قابلة للتعمية، وبما أنه لا يمكن استخدام سوى عناصر قابلة للتعمية مثل مفاتيح للقواميس، فمن غير الممكن استخدام صفٍ يحتوي على قائمة غير قابلة للتعمية مثل مفتاح. لنكتب ما يلي ضمن الصدفة التفاعلية: >>> tuple1 = ('cat', 'dog') >>> tuple2 = ('cat', ['apple', 'orange']) >>> spam = {} 1 >>> spam[tuple1] = 'a value' 2 >>> spam[tuple2] = 'another value' Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unhashable type: 'list' نلاحظ أن الصف tuple1 قابل للتعمية، في حين أن الصف tuple2 يتضمن قائمة فهو غير قابل للتعمية مثلها. أنواع الحاويات Containers والسلاسل المفهرسة بأرقام Sequences والمفهرسة بمفاتيح Mapping والمجموعات set تمتلك كلًا من الكلمات: حاوية وسلسلة مفهرسة بأرقام وسلسلة مفهرسة بمفاتيح معانٍ في بايثون قد لا تنطبق بالضرورة على غيرها من لغات البرمجة؛ فالحاوية في بايثون هي كائن من أي نوع بيانات، والتي قد تتضمن العديد من الكائنات الأخرى. تمثّل كلًا من القوائم والقواميس أنواعًا شائعة من الحاويات المستخدمة في بايثون. السلسلة المفهرسة بأرقام هي كائن يحتوي على أي من أنواع بيانات الحاويات، ذو قيم مرتبة يمكن الوصول إليها اعتمادًا على فهارس من أعداد صحيحة. السلاسل النصية والصفوف والقوائم والمتغيرات من النوع byte هي أنواع بيانات من نوع السلاسل المفهرسة بأرقام، إذ يمكن الوصول إلى القيم في هذا النوع من الكائنات باستخدام عامل الفهرسة (القوس المعقوف [ و ])، كما يمكن تمرير الفهرس إلى الدالة ()len. يُقصد بكلمة "مرتبة" وجود ما يمكن تسميته القيمة الأولى والثانية وهكذا، فعلى سبيل المثال، لا يمكن أن تكون قيمتا القائمتين التاليتين متساويتين لأنهما مرتبتان على نحوٍ مختلف: >>> [1, 2, 3] == [3, 2, 1] False السلسلة المفهرسة بمفاتيح mapping هي كائن يحتوي على أي من أنواع بيانات الحاويات الذي يستخدم المفاتيح عوضًا عن الفهرس. يمكن لهذا النوع من السلاسل أن يكون مرتبًا أو غير مرتبًا؛ فالقواميس في الإصدار 3.4 من بايثون والإصدارات الأقدم غير مرتبة لعدم وجود ما يسمى أول أو آخر زوج "مفتاح-قيمة" في القاموس، كما في المثال التالي: >>> spam = {'a': 1, 'b': 2, 'c': 3, 'd': 4} # This is run from CPython 3.5. >>> list(spam.keys()) ['a', 'c', 'd', 'b'] >>> spam['e'] = 5 >>> list(spam.keys()) ['e', 'a', 'c', 'd', 'b'] لا توجد ضمانات للحصول على العناصر من القواميس بترتيب متسق في الإصدارات القديمة من بايثون، ونتيجةً للطبيعة غير الترتيبية للقواميس، سيكون لقاموسين مجردين يحتويان على نفس أزواج "مفتاح-قيمة" ولكن بترتيب مختلف قيمتان متساويتان: >>> {'a': 1, 'b': 2, 'c': 3} == {'c': 3, 'a': 1, 'b': 2} True ولكن بدءًا من الإصدار 3.6 من المفسر CPython أصبحت القواميس تحتفظ بترتيب إدخال أزواج "مفتاح قيمة"، كما في المثال التالي: >>> spam = {'a': 1, 'b': 2, 'c': 3, 'd': 4} # This is run from CPython 3.6. >>> list(spam) ['a', 'b', 'c', 'd'] >>> spam['e'] = 5 >>> list(spam) ['a', 'b', 'c', 'd', 'e'] وتعد هذه إحدى الميزات الموجودة في الإصدار 3.6 من المفسّر CPython وليست موجودة في كافة مفسرات الإصدار 3.6 من بايثون، بينما تدعم كافة مفسرات الإصدار 3.7 من بايثون القواميس الترتيبية، والتي أصبحت معيارية في لغة بايثون في الإصدار 3.7. إلا أن حقيقة كون قاموس ما ترتيبي لا يعني أنه من الممكن الوصول إلى عناصره باستخدام فهارس من أعداد صحيحة، فمثلًا لا يُقيّم التعبير [spam[0 على أنه العنصر الأول من القاموس الترتيبي (فيما عدا حالة كون مفتاح العنصر الأول من القاموس هو فعلًا 0). تعد القواميس الترتيبية أيضًا متطابقة إذا كانت تحتوي على نفس أزواج "مفتاح-قيمة"، حتى ولو كانت هذه الأزواج بترتيب مختلف في كل قاموس. تتضمّن الوحدة collections العديد من السلاسل المفهرسة بمفاتيح الأخرى بما يتضمن OrderedDict (والتي تمثّل القواميس التي تحافظ على الترتيب دائمًا) و ChainMap (المُستخددمة لتجميع عدة قواميس معًا) و Counter (التي تُخزّن العناصر مثل مفاتيح للقواميس وعددها على أنها قيم فيه) و UserDict، وجميعها موصوفة ضمن توثيق بايثون. التوابع السحرية التوابع السحرية والتي تسمّى Dunder methods أو magic methods هي توابع خاصة في بايثون تبدأ أسماء كل منها وتنتهي بشرطتين سفليتين، وتُستخدم هذه التوابع لزيادة تحميل العامل. يأتي مصطلح Dunder اختصارًا للعبارة double underscore أي "شرطتين سفليتين". لعل أشهر التوابع السحرية هو ()__int__ والتي تقرأ بالشكل "dunder init dunder" أي "شرطتان سفليتان int شرطتان سفليتان" أو فقط "int"، والذي يعمل على تهيئة الكائنات. تتضمن بايثون عشراتٍ من التوابع السحرية. الوحدات والحزم الوحدة module هي برنامج بايثون تكون برامج بايثون الأخرى قادرةً على استيراده وبالتالي استخدام شيفرته. تسمّى مجموعة الوحدات الموجودة افتراضيًا في بايثون متجمعةً باسم مكتبة بايثون المعيارية، كما من الممكن إنشاء وحدات خاصة بك. إذا حفظت برنامجًا ما باسم "spam.py" على سبيل المثال، فمن الممكن تشغيل أمر الاستيراد import spam.py للوصول إلى الدوال والأصناف والمتغيرات عالية المستوى للبرنامج spam.py. الحزمة Package هي مجموعة من الوحدات التي نُشكّلها عبر إنشاء ملف باسم "init.py" ضمن المجلد المطلوب، إذ نستخدم اسم هذا المجلد اسمًا للحزمة، ويمكن أن تحتوي الحزم على وحدات متعددة (وهي ملفات ذات امتداد "py.") أو حتى حزم أخرى (أي مجلدات أخرى تحتوي على ملفات "init.py"). الكائنات من الدرجة الأولى First-Class Objects والقابلة للاستدعاء Callables لا تقتصر الأشياء القابلة للاستدعاء في بايثون على الدوال والتوابع، بل أي كائن يتضمّن عامل الاستدعاء -أي رمز القوسين الهلاليين ()- هو كائن قابل للاستدعاء. على سبيل المثال، لو كان لدينا التعبير ()def hello، يمكن فهم هذه الشيفرة على أنها متغير يدعى hello مُتضمنًا كائن دالة، إذ سيستدعي استخدام عامل الاستدعاء لهذا المتغير الدالة في المتغير بالشكل: ()hello. الأصناف classes هي من مفاهيم لغات البرمجة كائنية التوجه، ويمثّل الصنف مثالًا على كائن قابل للاستدعاء دون أن يكون دالة أو تابع، فعلى سبيل المثال، يمكن استدعاء صنف التاريخ date الموجود في الوحدة datatime باستخدام عامل الاستدعاء كما في الشيفرة (datetime.date(2020, 1, 1، وبمجرد استدعاء كائن الصنف، تعمل الشيفرة الموجودة ضمن التابع ()__int__ من الصنف. تعد الدوال من كائنات الدرجة الأولى في بايثون، ما يعني أنه يمكن تخزينها ضمن متغيرات وتمريرها مثل وسطاء عند استدعاء الدوال الأخرى واستخدامها مثل قيم مُعادة للدوال الأخرى أو تنفيذ كل ما يمكن تنفيذه مع أي كائن آخر، وبالتالي يمكن فهم التعليمة def الخاصة بتعريف الدوال على أنها عملية إسناد لكائن الدالة إلى متغير ما، إذ يمكن مثلًا إنشاء دالة ()spam لتُستدعى لاحقًا على النحو التالي: >>> def spam(): ... print('Spam! Spam! Spam!') ... >>> spam() Spam! Spam! Spam! كما يمكن إسناد كائن الدالة ()spam إلى متغيراتٍ أخرى، وبمجرد استدعاء المتغير الذي أسندنا الدالة إليه، ينفذ بايثون هذه الدالة: >>> eggs = spam >>> eggs() Spam! Spam! Spam! ويُدعى هذا بالأسماء المستعارة aliases، وهي أسماء جديدة مختلفة لدوال موجودة أصلًا، ونلجأ إلى هذه الطريقة عند الحاجة إلى إعادة تسمية دالة ما مع موجود أجزاء كبيرة من الشيفرة تستخدم الاسم القديم، ما يتطلب جهدًا كبيرًا لتعديل الاسم في كامل الشيفرة. ولعل الاستخدام الأكثر شيوعًا للدوال من الدرجة الأولى هو إمكانية تمرير الدوال مثل وسطاء لدوال أخرى، إذ يمكن مثلًا تعريف دالة باسم ()callTwicw لنمرر لها أي دالة نريد استدعائها مرتين، على النحو التالي: >>> def callTwice(func): ... func() ... func() ... >>> callTwice(spam) Spam! Spam! Spam! Spam! Spam! Spam! كما يمكن الاكتفاء باستدعاء الدالة ()spam مرتين بكتابتها مرتين في الشيفرة المصدرية، إلا أنه يمكن تمرير التابع المراد استدعائه مرتين إلى الدالة ()callTwicw عوضًا عن كتابة اسم الدالة مرتين في الشيفرة المصدرية. الخلاصة من السهل أن تمارس البرمجة لسنوات مع بقاء بعض المصطلحات البرمجية غير مألوفة بالنسبة لك، إلا أن معظم التطبيقات البرمجية قد أنشئت من قبل فريق كامل من المطورين وليس من قبل أفراد، لذا تُعد القدرة على التواصل الواضح أمرًا أساسيًا عند العمل ضمن فريق. وضحنا في هذا المقال أن برامج بايثون مكونة من معرفات ومتغيرات وسلاسل مجردة وكلمات مفتاحية وكائنات، وأن كل كائنات بايثون تمتلك قيمةً ونوع بيانات وهوية، ورغم كون كل كائن يمتلك نوع بيانات، إلا أنه يوجد مجموعة واسعة من فئات الأنواع، مثل الحاويات والسلاسل المفهرسة بأرقام وتلك المفهرسة بمفاتيح والمجموعات والمُعرف منها مسبقًا أو من قبل المستخدم. ستجد دومًا أن استخدام المصطلحات يختلف من لغة لأخرى وحتى من مبرمج لآخر، وستغدو معتادًا أكثر على المصطلحات مع الخبرة والبحوثات المتكررة على الإنترنت. ترجمة -وبتصرف- لجزء من الفصل السابع "المصطلحات البرمجية" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: الطرق البايثونية في استخدام قواميس بايثون ومتغيراتها وعاملها الثلاثي مفهوم الدوال Functions في البرمجة كيف تتعلم البرمجة: نصائح وأدوات لرحلتك في عالم البرمجة تعلم البرمجة
-
سنقدم في هذا المقال العديد من الطرق الشائعة لكتابة شيفرات بايثون الاصطلاحية إلى جانب نظيراتها من الطرق غير البايثونية فيما يتعلق بقواميس بايثون والتعامل مع المتغيرات وعاملها الثلاثي. تُعد القواميس dictionaries نواة العديد من برامج بايثون نظرًا للمرونة التي توفرها أزواج القيم المفتاحية key-value pairs عبر ربط أجزاء البيانات ببعضها بعضًا، وبالتالي من المفيد أن تتعلم حول اصطلاحات قواميس بايثون الأكثر استخدامًا في الشيفرات، ويمكنك مراجعها بقراءة مقال فهم القواميس في بايثون. استخدام التابعين ()get و ()setdefault للتعامل مع القواميس لدى محاولتك الوصول إلى مفتاح أحد القواميس دون وجوده، سيتسبب ذلك بخطأ من النوع KeyError، لذا يلجأ المبرمجون عادةً إلى تجنُّب هذه الحالة بكتابة شيفرات غير بايثونية، كما في المثال التالي: >>> # Unpythonic Example >>> numberOfPets = {'dogs': 2} >>> if 'cats' in numberOfPets: # Check if 'cats' exists as a key. ... print('I have', numberOfPets['cats'], 'cats.') ... else: ... print('I have 0 cats.') ... I have 0 cats. تتحقق الشيفرة السابقة من وجود السلسلة النصية cats مثل مفتاح ضمن القاموس المسمى numberOfPets؛ فإذا كانت كذلك فعلًا، يُطبع محتوى القاموس الموافق لهذا المفتاح باستخدام التعليمة ['numberOfPets['cats مثل جزء من الرسالة المعروضة للمستخدم باستخدام الدالة ()print؛ وفي حال عدم وجود هذا المفتاح تُطبع عبارة باستخدام دالة ()print أخرى دون الوصول إلى القاموس numberOfPetsكون المفتاح غير موجود وبالتالي لا تسبب بوقوع الخطأKeyError`. تمتلك قواميس بايثونتابع ()get، الذي يسمح بتحديد قيمة افتراضية لتعاد في حال طلب مفتاح غير موجود في القاموس. والشيفرة البايثونية التالية تكافئ الشيفرة في المثال السابق: >>> # Pythonic Example >>> numberOfPets = {'dogs': 2} >>> print('I have', numberOfPets.get('cats', 0), 'cats.') I have 0 cats. يتحقق الاستدعاء (numberOfPets.get('cats', 0 من كون المفتاح cats موجودًا في القاموس numberOfPets، فإذا كان موجودًا فعلًا يعيد الاستدعاء القيمة من القاموس الموافقة للمفتاح cats، وإلا يعيد الاستدعاء الوسيط الثاني وهو في حالتنا 0. إذًا، يمثّل استخدام التابع ()get لتحديد قيمة افتراضية لتعاد في حال استدعاء مفتاح غير موجود في القاموس طريقةً أقصر وأكثر مقروئية من استخدام العبارة الشرطية if-else. قد ترغب بالمقابل بتعيين قيمة افتراضية في حال عدم وجود مفتاح ما. على سبيل المثال، بفرض أن القاموس numberOfPets لا يتضمن المفتاح 'cats'، ستتسبب التعليمة numberOfPets['cats'] += 10 بخطأ من النوع KeyError، وقد ترغب بإضافة شيفرة مهمتها التحقق من غياب مفتاح محدد مُعينةً قيمة افتراضية على النحو التالي: >>> # Unpythonic Example >>> numberOfPets = {'dogs': 2} >>> if 'cats' not in numberOfPets: ... numberOfPets['cats'] = 0 ... >>> numberOfPets['cats'] += 10 >>> numberOfPets['cats'] 10 ولكن بما أن هذا النمط شائع الاستخدام أيضًا، فالقواميس تتضمن تابعًا أكثر بايثونية وهو ()setdefault. تكافئ الشيفرة التالية تلك الموجودة في المثال السابق: >>> # Pythonic Example >>> numberOfPets = {'dogs': 2} >>> numberOfPets.setdefault('cats', 0) # Does nothing if 'cats' exists. 0 >>> numberOfPets['cats'] += 10 >>> numberOfPets['cats'] 10 فبدلًا من استخدام جملة if شرطية للتحقق من وجود مفتاح ما في قاموس وتعيين قيمة افتراضية في حال غيابه، استخدم التابع ()setdefault. استخدام الصنف collections.defaultdict لتعيين قيم القواميس الافتراضية يتيح استخدام الصنف collections.defaultdict إمكانية التخلص التام من أخطاء KeyError، إذ يُمكّنك من إنشاء قاموس افتراضي عن طريق استيراد الوحدة collections واستدعاء الدالة ()collections.defaultdict مُمررًا إليها نمط معطيات تختاره ليكون قيمةً افتراضية. على سبيل المثال، في حال تمرير الدالة int مثل وسيط إلى الدالة ()collections.defaultdict تكون قد أنشأت كائنًا شبيهًا بالقاموس dictionary-like object يستخدم القيمة 0 قيمةً افتراضية في حال طلب مفاتيح غير موجودة في القاموس. دعنا نكتب التالي في الصدفة التفاعلية: >>> import collections >>> scores = collections.defaultdict(int) >>> scores defaultdict(<class 'int'>, {}) >>> scores['Al'] += 1 # No need to set a value for the 'Al' key first. >>> scores defaultdict(<class 'int'>, {'Al': 1}) >>> scores['Zophie'] # No need to set a value for the 'Zophie' key first. 0 >>> scores['Zophie'] += 40 >>> scores defaultdict(<class 'int'>, {'Al': 1, 'Zophie': 40}) مرّرنا في الشيفرة السابقة الدالة ()int ولم نستدعيها، وهذا سبب إهمالنا لأقواسها إذ كتبناها بالشكل int ضمن استدعاء الدالة (collections.defaultdict(int. يمكن تمرير الدالة list مثل وسيط وبالتالي استخدام قائمة فارغة بمثابة قيمة افتراضية للقاموس، كما في المثال التالي: >>> import collections >>> booksReadBy = collections.defaultdict(list) >>> booksReadBy['Al'].append('Oryx and Crake') >>> booksReadBy['Al'].append('American Gods') >>> len(booksReadBy['Al']) 2 >>> len(booksReadBy['Zophie']) # The default value is an empty list. 0 إذًا، في الحالات التي نحتاج فيها إلى قيمة افتراضية لكل احتمالية ممكنة للمفاتيح، فمن الأسهل استخدام الدالة ()collections.defaultdict بدلًا من استخدام قاموس عادي واستدعاء التابع ()setdefault. استخدام القواميس بدلا من العبارة Switch تمتلك لغات البرمجة مثل لغة جافا العبارة البرمجية switch، والتي تعد أحد أنواع العبارة الشرطية if-elif-else، إذ أنها تشغِّل الشيفرة بناءً على القيمة التي يتضمنها متغير معين من بين مجموعة قيم، أما لغة بايثون فلا تتضمن العبارة switch ما يجعل مبرمجي بايثون يكتبون في بعض الحالات شيفراتٍ كما في المثال التالي، والذي يشغّل مجموعة من تعليمات الإسناد على ضوء القيمة التي يتضمنها المتغير season من بين مجموعة قيم، على النحو التالي: # All of the following if and elif conditions have "season ==": if season == 'Winter': holiday = 'New Year\'s Day' elif season == 'Spring': holiday = 'May Day' elif season == 'Summer': holiday = 'Juneteenth' elif season == 'Fall': holiday = 'Halloween' else: holiday = 'Personal day off' لا يمكن عدّ الشيفرة السابقة غير بايثونية تمامًا، لكنها طريقة طويلة قليلًا، ومن الجدير بالذكر هنا أن عبارة switch في لغة جافا تفرض في سياقها استخدام تعليمة break في نهاية كل من كتلها، وبدونها سيتابع البرنامج تنفيذ الكتلة التالية الخاصة بالحالة التالية لقيمة المتغير، وبالتالي يمثّل نسيان استخدام التعليمة break مصدرًا شائعًا للأخطاء. في المقابل، تتكرر العبارات if-elif في مثالنا السابق، وهذا ما يجعل بعض المبرمجين يفضلون إنشاء قاموس بدلًا عنها. فيما يلي صيغة بايثونية مختصرة لنفس المثال السابق: holiday = {'Winter': 'New Year\'s Day', 'Spring': 'May Day', 'Summer': 'Juneteenth', 'Fall': 'Halloween'}.get(season, 'Personal day off') تتضمن الشيفرة السابقة تعليمة إسناد واحدة، إذ تمثّل القيمة المُخزّنة في المتغير holiday القيمة المعادة من استدعاء التابع ()get، الذي يعيد القيمة الموافقة للمفتاح المُعيّن في المتغير season، وفي حال كون المفتاح المطلوب غير موجود، سيعيد التابع ()get القيمة الافتراضية 'Personal day off'. جعل استخدام قاموس من الشيفرة مختصرة، إلا أنها قد تكون أصعب قليلًا للقراءة، ويعود القرار حول استخدامها من عدمه لك. التعابير الشرطية: عامل بايثون الثلاثي القبيح تعمل العوامل الثلاثية Ternary operators (والتي تُسمّى في بايثون رسميًا بالتعابير البرمجية الشرطية أو تعابير الانتقاء الثلاثية) على تقييم تعبير ما إلى إحدى قيمتين بناءً على شرط ما، وهذا ما نفعله عادةً باستخدام التعبير البايثوني if-else، على النحو التالي: >>> # Pythonic Example >>> condition = True >>> if condition: ... message = 'Access granted' ... else: ... message = 'Access denied' ... >>> message 'Access granted' تعني كلمة "ثلاثي Ternary" ببساطة أن العامل يستقبل ثلاثة مدخلات، إلا أن مرادفها في عالم البرمجة هو "التعبير الشرطي". تقدّم التعابير الشرطية صيغةً مختصرة تُكتب ضمن سطر واحد، وهذا الأمر مناسب لهذا النمط الثلاثي، وتُنفّذ هذه التعابير في بايثون من خلال توزيع معيّن لكل من الكلمتين المفتاحيتين ifو else، كما في المثال التالي: >>> valueIfTrue = 'Access granted' >>> valueIfFalse = 'Access denied' >>> condition = True 1 >>> message = valueIfTrue if condition else valueIfFalse >>> message 'Access granted' 2 >>> print(valueIfTrue if condition else valueIfFalse) 'Access granted' >>> condition = False >>> message = valueIfTrue if condition else valueIfFalse >>> message 'Access denied' يُقيّم التعبير ذو الرقم 1 إلى القيمة الموافقة للوسيط valueIfTrue في حال كون متغير الشرط condition محققًا، أي مُقيّم على أنه True؛ أما في حال كونه غير محقق، أي أنه مُقيّم على أنه False فيُقيّم التعبير إلى القيمة الموافقة للوسيط valueIfFalse. وصف مبتكر لغة بايثون هذا التعبير مازحًا بأنه "قبيح عمدًا intentionally ugly"، إذ تضع معظم لغات البرمجة المُتضمنة العوامل الثلاثية الشرط بدايةً متبوعًا بالتعليمات المطلوبة في حال تحققه ثم تلك المطلوبة في حال عدم تحققه، ولكن يمكن -مع هذه الطريقة- استخدام تعبير شرطي في أي موضع نريد مكان أي قيمة أو تعبير، حتى مثل وسيط لإحدى الدوال كما هو الحال في السطر 2 من الشيفرة السابقة. ما هو سبب اعتماد بايثون لهذه الصياغة في الإصدار 2.5 منها رغم كونها تنافي المبدأ التوجيهي الأول القائل "الجمال أفضل من القبح"؟ السبب هو أن العوامل الثلاثية شائعة الاستخدام بين المبرمجين رغم كونها ذات مقروئية منخفضة نسبيًا، لذلك طالبوا بايثون بدعمها. جديرٌ بالذكر أنه من الممكن إساءة استخدام اختصار العوامل المنطقية بغية إنشاء نوع من أنواع العوامل الثلاثية، إذ سيُقيّم تعبير مثل: condition and valueIfTrue or valueIfFalse إلى القيمة الموافقة للتعبير valueIfTrue في حال تحقق الشرط وإلى valueIfFalse في حال عدم تحققه (ماعدا حالة خاصة مهمة). لاحظ المثال التالي: >>> # Unpythonic Example >>> valueIfTrue = 'Access granted' >>> valueIfFalse = 'Access denied' >>> condition = True >>> condition and valueIfTrue or valueIfFalse 'Access granted' ينطوي العامل الثلاثي الزائف هذا على خطأ غامض خاص يحدث عند كون قيمة التعبير valueIfTrue من النمط الخاطئ (بمعنى أنها 0 أو False أو None أو سلسلة نصية فارغة)، فعندها سيُقيّم التعبير بصورة غير متوقعة إلى القيمة الموافقة للتعبير valueIfFalse عند تحقُق الشرط. استمر المبرمجون مع ذلك باستخدام هذا العامل الثلاثي الزائف، وأصبح السؤال "لماذا بايثون لا تتضمّن عاملًا ثلاثيًا؟" من الأسئلة الدائمة المطروحة على فريق مطوري بايثون الرئيسي. وكان من المتوقع أنه مع إنشاء التعابير الشرطية في بايثون سيتوقف المبرمجون عن طلب تضمين العامل الثلاثي وسيكفّون عن استخدام العامل الثلاثي الزائف المُتسبب بوقوع الخطأ. مع ذلك، تبقى التعابير الشرطية "قبيحة" بما يكفي لكي لا يتشجع المبرمجون على استخدامها، فرغم كون الجمال أفضل من القبح إلا أن العامل الثلاثي "القبيح" هو مثال على حكمة كون الواقع العملي غير مثالي. ليست التعابير الشرطية بايثونية تمامًا وليست غير بايثونية في الوقت نفسه، ففي حال استخدمتها، تجنب تداخلها: >>> # Unpythonic Example >>> age = 30 >>> ageRange = 'child' if age < 13 else 'teenager' if age >= 13 and age < 18 else 'adult' >>> ageRange 'adult' إذ تمثّل التعابير الشرطية خير مثال على أن السطور البرمجية المُكثّفة قد تكون صحيحة من الناحية البرمجية ولكنها صعبة الفهم والقراءة. التعامل مع قيم المتغيرات ستضطر غالبًا إلى التحقق من القيم المخزنة في المتغيرات وتعديلها، وتتضمن بايثون عدّة طرق لفعل ذلك مبينة من خلال المثالين التاليين. الإسناد التسلسلي وعوامل المقارنة في حال أردت التحقق من كون عدد ما ينتمي إلى مجال محدد، ستستخدم غالبًا العامل المنطقي and على النحو التالي: # Unpythonic Example if 42 < spam and spam < 99: إلا أن بايثون تسمح بتسلسل عوامل المقارنة وبالتالي من الممكن الاستغناء عن العامل and. تكافئ الشيفرة التالية تلك المذكورة في المثال السابق: # Pythonic Example if 42 < spam < 99: ينطبق الأمر ذاته على تسلسل عامل الإسناد =، إذ من الممكن تعيين عدد من المتغيرات إلى نفس القيمة ضمن سطر برمجي واحد، على النحو التالي: >>> # Pythonic Example >>> spam = eggs = bacon = 'string' >>> print(spam, eggs, bacon) string string string يمكن استخدام العامل and للتحقق من كون المتغيرات الثلاث تخزّن فعلًا نفس القيمة، أو استخدام سلسلة من العامل == للتحقق من المساواة ببساطة أكبر. >>> # Pythonic Example >>> spam = eggs = bacon = 'string' >>> spam == eggs == bacon == 'string' True ميزة تسلسل العوامل بسيطة، إلا أنها ذات فائدة كبيرة في بايثون، ولكن بالمقابل سيتسبب الاستخدام الخاطئ لها بالأخطاء. التحقق من كون قيمة متغير تنتمي لمجموعة محددة من القيم قد تصادف أحيانًا حالة معاكسة تمامًا لتلك الواردة في الفقرة السابقة: وهي التحقق من كون قيمة متغير ما هي واحدة من مجموعة قيم ممكنة، ومن الممكن إنجاز ذلك باستخدام العامل or كما في التعبير التالي: spam == 'cat' or spam == 'dog' or spam == 'moose` إلا أن الجزء المتكرر ==spam يجعل من التعبير السابق غير عملي الاستخدام، ويمكن بدلًا من ذلك وضع القيم المحتملة ضمن بنية صف والتحقق من كون قيمة متغير ما موجودةً ضمن هذا الصف باستخدام العامل in، كما في المثال التالي: >>> # Pythonic Example >>> spam = 'cat' >>> spam in ('cat', 'dog', 'moose') True لا تمتاز هذه الطريقة بالسهولة فحسب، بل إنها أسرع أيضًا من حيث الوقت اللازم. الخلاصة ما من لغة برمجة إلا ولديها الاصطلاحات والممارسات الأفضل الخاصة بها، وقد ركّز هذا المقال على الطرق العملية التي يستخدمها مبرمجو بايثون لكتابة شيفرات "بايثونية" للتعامل مع القواميس والمتغيرات وعامل بايثون الثلاثي. تمتلك القواميس التابعين ()get و ()setdefault بغية التعامل مع المفاتيح غير الموجودة، ولكن من الممكن استخدام قاموس من النوع collections.defaultdict لتخزين القيم الافتراضية للمفاتيح غير الموجودة. لا تتضمن لغة بايثون العبارة switch، لكن يمثّل استخدام القواميس طريقةً مختصرة لتطبيق ما يكافئ استخدام العبارة switch دون الحاجة إلى استخدام عدد كبير من عبارات if-elif-else، كما يمكن استخدام العوامل الثلاثية عند الحاجة إلى تقييم شرط ما إلى إحدى قيمتين. وتتحقق العوامل == من كون المتغيرات متساوية أم لا، في حين يتحقق العامل in من كون المتغير يمثل واحدة من مجموعة قيم ممكنة. ترجمة -وبتصرف- لجزء من الفصل السادس "كتابة شيفرات بايثون" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: كتابة شيفرات بايثون: صيغ شائعة الاستخدام على نحو خاطئ اتخاذ القرار: العبارات الشرطية والحلقات التكرارية في البرمجة فهم العمليات المنطقية في بايثون
-
ما من لغة برمجة إلا وتصف نفسها بالقوة، الصفة عديمة المعنى في عالم لغات البرمجة، حتى أن كتيب بايثون التعليمي الرسمي يبدأ بالعبارة "بايثون هي لغة برمجة قوية سهلة التعلم". ولكن ما من خوارزمية تنفيذها حكر على لغة برمجة دون غيرها، وما من وحدة قياس لتحديد مدى "قوة" لغة برمجة ما (ولكن من الممكن بالطبع قياس الكم الذي يجادل به المبرمجون دفاعًا عن لغتهم المفضلة). إلا أن كل لغة تتميز بأنماطها التصميمية الخاصة وبثغراتها، ما يشكّل بالنتيجة نقاط قوتها وضعفها. ولكتابة شيفرات بايثون كالمحترفين، فلا بدّ من معرفتك لما يتجاوز حدود قواعدها النحوية ومكتباتها المعيارية. وتتمثّل الخطوة التالية بتعلّم الاصطلاحات أو الممارسات الخاصة بكتابة الشيفرات في بايثون. إذ تسمح بعض ميزات لغة بايثون لنفسها بكتابة الشيفرات باستخدام طرق خاصة بلغة بايثون والتي غدت معروفة باسم الطرق البايثونية Pythonic. سنقدم في هذا المقال العديد من الطرق الشائعة لكتابة شيفرات بايثون الاصطلاحية إلى جانب نظيراتها من الطرق غير البايثونية. فما يعد "بايثوني" قد يختلف من مبرمج لآخر. مبادئ بايثون التوجيهية العشرون مبادئ بايثون التوجيهية العشرون أو ما يعرف باسم "زن بايثون The Zen of Python" الموضوعة من قبل تيم بيترز Tim Peters تختص بتصميم لغة بايثون وبرامجها. وليس من الضروري أن تتبع في برامجك كامل هذه التوجيهات، إلا أنه من المفيد تذكرها دومًا. كما أنها تمثل هديةً مخفية أو طرفة كامنة تظهر لدى تشغيل الأمر import this كما يلي: >>> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. --snip-- ** ملاحظة**: الغامض في الأمر أن عدد المبادئ التوجيهية التي كتبها تيم بيترز هو تسعة عشر. وفي هذا الصدد يقال أنّ مُنشئ بايثون جيدو فان روسوم قد قال بأن الحكمة رقم 20 المفقودة ما هي إلا بعض من غرائب وطرائف تيم بيترز، إذ تركها تيم لجيدو ليملأها، الأمر الذي لم يقم به الأخير على ما يبدو. وبالنتيجة هذه المبادئ التوجيهية لا تتعدى كونها آراء يمكن للمبرمجين أن يؤيدوها أو يعارضوها. وكأي مجموعة من المبادئ الجيدة فإنها تناقض نفسها موفرةً أكبر قدر ممكن من المرونة. وفيما يلي تفسيري لهذه الحكم: لعل الشيفرة الجميلة هي تلك التي تسهل قراءتها وفهمها. فغالبًا ما يكتب المبرمجون شيفراتهم بسرعة غير مكترثين بمدى قابليتها للقراءة، فالحاسوب سيشغلها على أية حال، إلا أن تصحيحها أو اكتشاف الأخطاء فيها سيكون أمرًا صعبًا بالنسبة للمبرمجين الآخرين. وجمال الشيفرة بالنتيجة هو أمر شخصي، إلا أن الشيفرة المكتوبة دون اكتراث لمدى قابليتها للفهم ستكون في نظر الآخرين قبيحة حتمًا. ولعل أهم سبب لشعبية بايثون هو كون شيفراتها لا تعمها الفوضى بعلامات ترقيم بلا جدوى كغيرها من لغات البرمجة ما يسهل التعامل معها. لنفرض أني كتبت الآن "القول مُفسّر لنفسه" كشرح لهذه الحكمة، أليس بالشرح المريع؟ كذلك الأمر بالنسبة للشيفرات، فمن المحبذ أن تتبع الأساليب المطولة الصريحة، متجنبًا إخفاء آليات عمل الشيفرة تحت قناع ميزات اللغة التي تتطلب معرفة عميقة بها لفهمها. تُذكرنا هاتان الحكمتان بحقيقة أنه من الممكن إتمام أي أمر باستخدام تقنيات بسيطة أو معقدة. لنفرض أنه لديك عمل بسيط يتطلب إنجازه استخدام مجرفة يدوية، عندها سيكون استخدام مجرفة هيدروليكية بسعة 50 طن لإنجازه ضرب من المبالغة. أما إذا كان العمل المطلوب ضخمًا جدًا، فإن تعقيدية استخدام مجرفة هيدروليكية واحدة يبقى أفضل من التعقيدية المتشعبة التي تكتنف التنسيق بين فريق مكون من 100 جرافة يدوية لإنجاز العمل. اجعل الأفضلية دومًا للبساطة على التعقيد، شرط أن تعرف حدود البساطة. يميل المبرمجون لتنظيم شيفرتهم ضمن فئات، لاسيما تلك الفئات التي تتضمن فئات فرعية والتي تتضمن بدورها فئات فرعية أيضًا. ولا تضيف هذه البنى الهرمية عادةً إلى الشيفرة تنظيمًا بقدر ما تضيف له بيروقراطية. ولا بأس بأن تكتب شيفرتك ضمن إحدى الوحدات عالية المستوى أو ضمن بنية معطيات واحدة. ولكن إن بدت شيفرتك بالشكل ()spam.eggs.chicken.fish أو ['spam['eggs']['chicken']['fish، فاعلم أن شيفرتك شديدة التعقيد والتشعب. يميل المبرمجون عادةً إلى حشر أكبر كم ممكن من التعليمات ضمن أصغر جزء ممكن من الشيفرة، كما في السطر البرمجي: print('\n'.join("%i bytes = %i bits which has %i possiblevalues." % (j, j*8, 256**j-1) for j in (1 << i for i in range(8)))) فرغم كون شيفرة كهذه قد تكون محط إعجاب الأصدقاء، إلا أنها حتمًا ستكون مصدر إزعاج لمن يتوجب عليه فهمها من زملاء العمل. فلا تجعل شيفرتك تنفذ مهام عديدة معًا، إذ أن الشيفرات الموزعة على أسطر متعددة عادةً ما تكون أسهل للقراءة من تلك المحشورة في سطر واحد ضمن مسافة ضيقة. هذه الحكمة تصب في نفس معنى تلك القائلة البساطة أفضل من التعقيد. رغم كون ()strcmp تشير وضوحًا إلى دالة مقارنة السلاسل المحرفية String Compare لشخص يعمل في ميدان البرمجة بلغة سي C منذ السبعينيات، إلا أن الحواسيب في أيامنا تملك ما يكفي من الذاكرة لكتابة أسماء الدوال كاملةً. فلا تحذف أحرفًا من أسماء المعرفات ولا تبالغ في اختصار شيفرتك. خذ وقتك لاختيار أسماء المتغيرات والدوال لتكون وصفية ومحددة. كما أن تضمين سطر فارغ ما بين الأقسام المختلفة من شيفرتك أمر يماثل بأهميته فاصل الفقرات في الكتب المطبوعة، إذ يوضح للقارئ الأجزاء المتوجب قراءتها معًا ككتلة واحدة. هذه الحكمة تصب في نفس معنى تلك القائلة الجمال أفضل من القبح. تنطوي هاتان الحكمتان على شيء من التناقض. فمن ناحية عالم البرمجة مليء بما يسمى "أفضل الممارسات" best practices التي على المبرمجين الانصياع لها ما أمكن أثناء كتابة شيفراتهم، إلا أن الالتفاف على هذه الممارسات كالتفاف سريع قد يكون أمرًا مغريًا لكنه قد يسبب المزيد من الفوضى والتشابك لتكون الشيفرة بالنتيجة غير مُتسقة وذات مقروئية منخفضة. ومن ناحية أُخرى سيؤدي الالتزام المبالغ به بالقواعد بغض النظر عن خصوصية الواقع إلى شيفرة مجردة وبمقروئية منخفضة أيضًا. فعلى سبيل المثال، غالبًا ما يؤدي سعي لغة جافا لموائمة كافة شيفراتها وفقًا لنموذجها كائني التوجه إلى شيفرات متداولة كثيرة حتى لأصغر البرامج. الحل إذًا بإمساك العصا من المنتصف ما بين الحكمتين، الأمر الذي سيغدو أسهل مع تراكم خبراتك، فمع الوقت لن تتعلم القواعد فحسب، بل ستتعلم متى يمكنك كسرها. حقيقة كون المبرمجين يميلون لتجاهل رسائل الأخطاء لا تعني أن البرامج يجب أن تتوقف عن إصدارها. تحدث الأخطاء الصامتة عندما تعيد الدوال شيفراتٍ خاطئة أو قيمة خالية None بدلًا من التعامل مع الاستثناءات. ترشدنا هاتان الحكمتان إلى حقيقة أنه من الأفضل للبرنامج أن يفشل ويتوقف عن العمل بسرعة على أن نُسكت الخطأ ونتابع التنفيذ، ففي هذه الحالة كل ما نفعله هو تأجيل حتمية وقوع الخطأ لاحقًا وسيكون حينها تنقيحه أصعب نظرًا لاكتشافه بعد فترة طويلة من وقوع سببه الفعلي. فمن الممكن أن تقرر تجاهل رسائل الأخطاء الناتجة عن برامجك دومًا، لكن تأكد من كونك تفعل ذلك لسبب وجيه. جعلت الحواسيب البشر ميالين لتصديق الخرافات: فلحل أعقد المشاكل في حواسيبنا غدونا نمارس طقسنا الأشهر في إعادة تشغيلها، الأمر الذي سيصلح أي مشكلة مبهمة، إلا أن الواقع مختلف، فالحواسيب ليست مسحورة، وإن كانت شيفرتك لا تعمل فلابد من وجود سبب لن يكتشفه ويحل المشكلة سوى التفكير النقدي التفصيلي. تجنّب تجربة الحلول العشوائية العمياء وصولًا لعمل الشيفرة بأي وسيلة، فبهذه الطريقة أنت تخفي المشكلة فقط بدلًا من حلها جذريًا. تمثل هذه الحكمة انتقاد لشعار لغة البرمجة Perl القائل: "هناك أكثر من طريقة لإنجاز الأمر!" إذ تبيّن أن وجود ثلاث أو أربع طرق مختلفة لكتابة شيفرة تؤدي نفس الغرض هو سيف ذو حدين، فمن ناحية هذه الميزة تمنحك المرونة في كيفية كتابة الشيفرات، إلا أنها تتطلب منك تعلم كافة الطرق الممكنة للكتابة حتى تصبح قادرًا على قراءة شيفرات الآخرين. وبالتالي فإنّ هذه المرونة لا تستحق عناء الجهد الإضافي اللازم خلال تعلم لغة البرمجة. هذه الحكمة عبارة عن طرفة، إشارةً لكون مبتكر بايثون جيدو فان روسوم هولندي الأصل. تشير هاتان الحكمتان لحقيقة كون الشيفرة التي تُنفّذ ببطء هي أسوأ وضوحًا من تلك التي تُنفّذ سريعًا. ولكن بالمقابل من الأفضل الانتظار بعض الوقت لحين تنفيذ برنامجك على أن يُنفّذ بسرعة وبنتائج خاطئة. تتعقد الكثير من الأمور بمرور الوقت، كالقوانين الضريبية والعلاقات العاطفية وكتب بايثون البرمجية! والأمر نفسه ينطبق على عالم البرمجيات. تذكرنا هاتان الحكمتان بحقيقة أنه في حال كون الشيفرة معقدة لدرجة تجعل من المستحيل للمبرمجين الآخرين فهمها وتنقيحها، فهي شيفرة سيئة. ولكن بالمقابل فإن سهولة شرح فكرة الشيفرة للآخرين لا يعني بالضرورة أنها ليست سيئة. فللأسف معرفة كيفية جعل الشيفرة بسيطة ما أمكن بطريقة مدروسة ليست بالمهمة السهلة. تعد نطاقات الأسماء عبارة عن حافظات منفصلة للمُعرفات مهمتها تجنب حدوث تعارضات في الأسماء. فمثلًا لكل من ()open و()webbrowser.open الاسم نفسه إلا أنهما تشيران لدالتين مختلفتين. فاستيراد متصفح الويب باستخدام الدالة ()webbrowser.open لا يتعارض ودالة بايثون ()open نظرًا لكون كل منهما ينتمي لنطاق أسماء مختلف، وهما نطاق أسماء الدوال الخاصة ببايثون ونطاق أسماء وحدة متصفح الويب. ومن الضروري في هذا الصدد تذكر الحكمة القائلة أن السطحية أفضل من التداخل، فمع روعة استخدام نطاقات الأسماء، فلا يجب استخدامها إلا بهدف منع تعارضات الأسماء، وليس بغية إضافة تنظيم إضافي ضمن فئات دون مبرر. وككل الآراء في عالم البرمجة، قد لا تتفق والآراء المبينة أعلاه أو قد تعبّر عما هو مخالف لرأيك وموقفك، ولكن تذكر دائمًا بأن الجدالات حيال كيفية كتابة الشيفرات وما يعتبر منها بايثونيًا -وعلى خلاف ما تظنه- نادرًا ما يكون مثمرًا (ما لم تكن تؤلف كتابًا كاملًا مليئًا بالآراء البرمجية). دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن اعتد استخدام المسافات البادئة ذات المعنى لعل مبعث القلق الأكثر شيوعًا الذي نسمعه حول بايثون من المبرمجين المعتادين على استخدام لغات برمجة أخرى هو أن المسافات البادئة الإلزامية في بايثون (والتي تسمى خطأً المسافات الإلزامية) غريبة وغير مألوفة. إلا أن مقدار المسافة البادئة في بداية السطر البرمجي ذو معنى في بايثون، إذ يحدد السطور البرمجية المنتمية لكتلة واحدة من الشيفرة. وقد يبدو توزيع كتل التعليمات ضمن مجموعات بالاعتماد على المسافات البادئة أمرًا غريبًا في بايثون، إذ تبدأ الكتل وتنتهي في لغات البرمجة الأخرى باستخدام الأقواس المعقوصة { و }. ولكن حتى المبرمجين ممن يستخدمون لغات برمجة غير بايثون عادةً ما يبدأون الكتل البرمجية بمسافة بادئة كما هو الحال مع مبرمجي بايثون، ما يجعل من مقروئية شيفراتهم أعلى. فعلى سبيل المثال، لغة جافا لا تتضمّن مفهوم إلزامية استخدام المسافات البادئة، ومع ذلك يميل مستخدموها لاستخدام المسافات البادئة بغية زيادة مقروئية شيفراتهم. يتضمن المثال التالي دالة جافا باسم ()main تحتوي على استدعاء وحيد لدالة ()println: // Java Example public static void main(String[] args) { System.out.println("Hello, world!"); } ستعمل شيفرة جافا السابقة كما يجب حتى في حال عدم استخدام مسافة بادئة لسطر الدالة ()println، وذلك لأن الأقواس المعقوصة تحدد بداية ونهاية الكتل البرمجية في جافا عوضًا عن المسافات البادئة. وعلى خلاف جافا التي جعلت من استخدام المسافات البادئة أمرًا اختياريًا، فرضت بايثون جعل الشيفرات مقروءة دومًا بإلزامية استخدام المسافات البادئة. مع ملاحظة أن بايثون لا تفرض استخدام المسافات البيضاء، إذ أنها لا تفرض أي قيود على استخدام المسافات بيضاء غير المُمثلة لمسافات بادئة (فكلا التعبيرين 2+2 و2 + 2 يعملان في بايثون). وأحد السجالات البرمجية هو وجوب وضع القوس الاستهلالي على نفس السطر مع العبارة البرمجية الاستهلالية أم في السطر التالي، سيجادل كل مبرمج مدافعًا عن أسلوبه المفضل حتى النهاية، الأمر الذي تجنبته بايثون عن عمد من خلال عدم استخدام الأقواس بالمطلق، ما يجعل مبرمجي بايثون الأكثر إنتاجية، ولكم تمنيت أن تتبنى كافّة لغات البرمجة منهجية بايثون في تجميع الكتل البرمجية في الشيفرات. ومع ذلك يتوق البعض إلى الأقواس متمنين إضافتها إلى أحد إصدارات بايثون المستقبلية رغم مدى كونها غير بايثونية، ناهيك عن كون وحدات بايثون المستقبلية تُطبق تحديثاتها وميزاتها على الإصدارات الأقدم، ولدى محاولتك استيراد ميزة استخدام الأقواس في بايثون، ستحصل على المفاجأة التالية: >>> from __future__ import braces SyntaxError: not a chance لن تضاف الأقواس إلى بايثون على المدى المنظور. صيغ شائعة الاستخدام على نحو خاطئ إذا لم تكن لغة بايثون هي أولى اللغات البرمجية التي تعلمتها، فبإمكانك كتابة شيفراتها وفق الاسترتيجيات التي اعتدت استخدامها مع لغات البرمجة الأخرى، أو لربما أنك قد تعلمت طرقًا غير معهودة لكتابة شيفرات بايثون نظرًا لعدم درايتك بوجود ممارساتٍ راسخة مُحبذة، ستعمل شيفرتك المكتوبة بالطرق غير المعهودة، ولكن من الممكن أن توفر بعضًا من الوقت والجهد بتعلمك للمزيد من المنهجيات المعيارية لكتابة شيفرات بايثونية. يشرح هذا القسم أخطاء المبرمجين الشائعة وكيفية كتابة الشيفرات متجنبًا الوقوع بها. استخدم الدالة ()enumerate بدلا من ()range يستخدم بعض المبرمجون الدالتين ()range و()len لدى الحاجة إلى المرور على عناصر قائمة أو غيرها من البنى المتسلسلة بغية توليد الأرقام الصحيحة الدالة على فهرس (ترتيب) العناصر ابتداءً من الصفر ووصولًا إلى ما قبل طول السلسلة، ومن الشائع استخدام متغير باسم i للدلالة على الفهرس في حلقات for التكرارية هذه. فعلى سبيل المثال، بكتابة الشيفرة غير البايثونية في الصدفة التفاعلية سنحصل على الخرج المبين أدناه: >>> animals = ['cat', 'dog', 'moose'] >>> for i in range(len(animals)): ... print(i, animals[i]) ... 0 cat 1 dog 2 moose قد يكون الاصطلاح (()range(len واضح، لكنه لا يرقى لأن يعد مثاليًا نظرًا لصعوبة قراءته. فبدلًا من ذلك من الممكن تمرير القائمة أو السلسلة إلى دالة بايثون ()enumerate، والتي ستعيد عددًا صحيحًا دالًا على رقم الفهرس مع قيمة العنصر الذي يشير إليه كل فهرس. فعلى سبيل المثال، من الممكن كتابة الشيفرة البايثونية التالية وصولًا إلى نفس النتيجة السابقة: >>> # Pythonic Example >>> animals = ['cat', 'dog', 'moose'] >>> for i, animal in enumerate(animals): ... print(i, animal) ... 0 cat 1 dog 2 moose وبذلك ستكون شيفرتك أفضل مع استخدام الدالة ()enumerate عوضًا عن التركيب (()range(len. وفي حال رغبتك بطباعة عناصر القائمة فقط دون رقم فهرس كل منها، فمن الممكن أيضًا المرور على عناصر القائمة بطريقة بايثونية كما يلي: >>> # Pythonic Example >>> animals = ['cat', 'dog', 'moose'] >>> for animal in animals: ... print(animal) ... cat dog moose فكل من استخدام الدالة ()enumerate والمرور المباشر على عناصر السلسلة هي أمور مفضلة على استخدام التركيب التقليدي (()range(len. استخدام التعليمة with بدلا من الدالتين()open و()close تعيد الدالة ()open كائن ملف يتضمّن التوابع اللازمة للقراءة من ملف أو الكتابة فيه، وعند انتهائك يعمل التابع ()close الخاص بكائن الملف على إتاحة الملف للبرامج الأخرى لتقرأ منه أو تكتب فيه. كما من الممكن استخدام كل من هاتين الدالتين منفردة، إلا أن هذه الطريقة ليست بايثونية. فعلى سبيل المثال، لنُدخل الشيفرات التالية في الصدفة التفاعلية بغية كتابة العبارة النصية "!Hello, World" ضمن الملف المسمى spam.txt: >>> # Unpythonic Example >>> fileObj = open('spam.txt', 'w') >>> fileObj.write('Hello, world!') 13 >>> fileObj.close() كتابة الشيفرة بهذه الطريقة قد تودي بالنتيجة إلى إبقاء الملف مفتوحًا وغير متاحًا للبرامج الأخرى، ففي حال حدوث خطأ في كتلة try مثلًا، سيتجاهل الملف استدعاء الدالة ()close كما في المثال التالي: >>> # Unpythonic Example >>> try: ... fileObj = open('spam.txt', 'w') ... eggs = 42 / 0 # A zero divide error happens here. ... fileObj.close() # This line never runs. ... except: ... print('Some error occurred.') ... Some error occurred. فبمجرد الوصول إلى خطأ القسمة على صفر سينتقل التنفيذ مباشرةً إلى الكتلة except، متجاوزًا استدعاء الدالة ()close تاركًا بذلك الملف مفتوحًا، ما قد يؤدي بالنتيجة إلى أخطاء تلف الملفات file corruption لاحقًا، ومن الصعب تعقب الخطأ وتوقّع أن مصدره هو كتلة try. وبدلًا من الطريقة السابقة من الممكن استخدام التعليمة with والتي تستدعي الدالة ()close تلقائيًا بمجرد انتهاء تنفيذ الكتلة with. وفيما يلي مثال مكتوب بطريقة بايثونية يؤدي نفس مهمة المثال الأول من هذه الفقرة: >>> # Pythonic Example >>> with open('spam.txt', 'w') as fileObj: ... fileObj.write('Hello, world!') … فرغم عدم استدعاء الدالة ()close صراحةً، إلا أن التعليمةwith ستستدعيها نلقائيًا بمجرد انتهاء تنفيذ الكتلة البرمجية هذه. استخدم المعامل is بدلًا من == للمقارنة مع القيمة الخالية None يقارن معامل المساواة == قيمتي كائنين، في حين أن معامل التماثل is يقارن تطابق هويات الكائنات. فمن الممكن أن يتضمّن كائنين قيمًا متكافئة ولكن كونهما كائنان منفصلان فهذا يعني أن لكل منهما هويته المنفصلة عن الآخر. وعمومًا في حال مقارنة قيمة ما مع القيمة الخالية None استخدم دائمًا المعامل is عوضًا عن المعامل ==. فقد يُقيّم التعبير spam==None على أنه صحيح True في بعض الحالات حتى في حال كون المتغير spam فارغًا بالمعنى المجرد، الأمر الناتج عن زيادة تحميل المعامل ==. في حين أن التعبير spam is None سيتحقق من كون القيمة في المتغير spam هي حرفيًا None، إذ أنّ None هي القيمة الوحيدة ضمن نمط المعطيات الخالية None Type، فلا يوجد سوى كائن خالٍ None Object واحد في أي برنامج بايثون. فإن عُيّن أحد المتغيرات ليكون خاليًا None، فعندها سيُقيّم التعبير is None دومًا على أنه صحيح True، وفيما يلي مثال على حالة زيادة تحميل المعامل: >>> class SomeClass: ... def __eq__(self, other): ... if other is None: ... return True ... >>> spam = SomeClass() >>> spam == None True >>> spam is None False ورغم كون إمكانية أن يتسبب صنفًا بزيادة تحميل المعامل == بهذه الطريقة أمر نادر الحدوث، إلا أنه سبب وجيه لاصطلاح بايثون باستخدام التعبير is None عوضًا عن None == في هذه الحالات. ونهايةً، لا تستخدم المعامل is مع القيمتين True وFalse بل استخدم المعامل == لمقارنة هذه القيم، من قبيل spam == True أو Spam == False. والطريقة الأكثر شيوعًا في مثل هذه الحالات هي عدم استخدام المعامل والقيمة المنطقية بالمطلق، والاكتفاء بكتابة الشيفرة بالشكل :if spam أو :if not spam بدلًا من كتابة :if spam == True أو :if spam == False. تنسيق السلاسل النصية تظهر السلاسل النصية تقريبًا في كل برنامج بغض النظر عن لغة البرمجة المستخدمة، فهي من أنواع البيانات الشائعة، فمن المتوقع وجود العديد من المنهجيات لمعالجة السلاسل النصية وتنسيقها. يسلط هذا القسم الضوء على اثنين من أفضل الممارسات بهذا الخصوص. استخدم تنسيق السلاسل النصية الخام إذا تضمنت السلاسل عدة خطوط مائلة عكسية تمكننا محارف الهروب escape characters من إدخال النصوص ضمن صياغة السلسلة النصية والتي يستحيل تضمينها فيها دون استخدامها. فعلى سبيل المثال لابد من استخدام محرف الهروب \ في العبارة Ahmad\'s chair لتُفسر علامة الاقتباس الثانية على أنها جزء من السلسلة النصية وليست على أنها علامة انتهاء السلسلة. ولكن ماذا لو أردنا بالفعل تضمين الرمز \ بحد ذاته ضمن السلسلة النصية؟ عندها يجب استخدامه بالشكل \\. السلاسل النصية الخام عبارة عن طريقة لصياغة السلاسل النصية باستخدام البادئة r، وتتميز بأنها لا تعامل الخطوط المائلة العكسية كمحارف هروب، بل تعاملها كأي محرف من السلسلة نفسها. فعلى سبيل المثال، فيما يلي مسار ملف في بيئة ويندوز والذي يتطلب استخدام العديد من الخطوط المائلة العكسية كمحارف هروب لتضمين الفعلية منها في السلسلة النصية، الطريقة التي لا يمكن عدها بايثونية: >>> # Unpythonic Example >>> print('The file is in C:\\Users\\Al\\Desktop\\Info\\Archive\\Spam') The file is in C:\Users\Al\Desktop\Info\Archive\Spam أما باستخدام مفهوم السلاسل النصية الخام (لاحظ البادئة r) فسنحصل بالنتيجة على نفس السلسلة ولكن مع شيفرة ذات مقروئية أعلى: >>> # Pythonic Example >>> print(r'The file is in C:\Users\Al\Desktop\Info\Archive\Spam') The file is in C:\Users\Al\Desktop\Info\Archive\Spam لا يمكن عد السلاسل النصية الخام raw strings كنمط بيانات مستقل عن السلاسل النصية، فما هي سوى طريقة مناسبة لكتابة السلاسل النصية المُجردة المتضمنة للعديد من الخطوط المائلة العكسية، وعادة ما نستخدم السلاسل النصية الخام في كتابة التعابير العادية المستخدمة في تشكيل أنماط البحث أو في كتابة مسارات ملفات ويندوز والتي غالبًا ما تتضمن العديد من الخطوط المائلة العكسية، إذ سيكون من الصعب استخدام محرف هروب لكل منها بالشكل \\. نسق السلاسل النصية باستخدام السلاسل النصية التنسيقية F-Strings يُعرّف تنسيق السلاسل المحرفية أو معالجتها بأنه عملية إنشاء سلاسل محرفية تتضمن سلاسل محرفية أخرى، الأمر الذي مر بالعديد من المراحل خلال تاريخ بايثون. فمن الممكن استخدام المعامل + لربط السلاسل المحرفية معًا، إلا أنها ستعطي بالنتيجة شيفرة مليئة بإشارات التنصيص وعلامات الزائد من قبيل: 'Hello, ' + name + '. Today is ' + day + ' and it is ' + weather + '.' كما من الممكن استخدام مُحدّد التحويل %s الذي يجعل من الصياغة أسهل بعض الشيء بالشكل: 'Hello, %s. Today is %s and it is %s.' % (name, day, weather) وبالنتيجة ستعمل كلا الطريقتين على إدخال السلاسل النصية الموافقة مكان كل من المتغيرات name وday وweather في السلسلة النصية المجردة وصولًا إلى سلسلة نصية جديدة مثل: 'Hello, Al. Today is Sunday and it is sunny.' يعمل التابع ()format الخاص بالسلاسل المحرفية على تمكين لغة تخصيص التنسيق المصغرة Format Specification Mini-Language والتي تتضمن استخدام أزواج الأقواس المعقوصة {} بطريقة مماثلة لفكرة استخدام مُحدّد التحويل s%، إلا أن هذه الطريقة تنطوي على شيء من التعقيد ما قد يُنتج شيفرة بمقروئية منخفضة، لذا لا أشجع استخدامها. إلى أن جاء الإصدار 3.6 من بايثون بميزة السلاسل النصية التنسيقية f-strings (اختصارًا لعبارة format strings) التي توفر طريقة أكثر ملائمة لإنشاء سلاسل نصية تتضمن سلاسل نصية أخرى. وكما هو الحال مع السلاسل النصية الخام والتي نستخدم لإنشائها البادئة r قبل علامة الاقتباس الاستهلالية، نستخدم هنا البادئة f، وفيها نُضمّن أسماء المتغيرات المطلوبة ضمن أقواس معقوصة لاستبدال كل منها لاحقًا بالسلاسل النصية المُخزنة فيها، على النحو: >>> name, day, weather = 'Al', 'Sunday', 'sunny' >>> f'Hello, {name}. Today is {day} and it is {weather}.' 'Hello, Al. Today is Sunday and it is sunny.' كما من الممكن تضمين تعابير برمجية كاملة في الأقواس المعقوصة، كما في المثال: >>> width, length = 10, 12 >>> f'A {width} by {length} room has an area of {width * length}.' 'A 10 by 12 room has an area of 120.' وفي حال رغبتك باستخدام أقواس معقوصة فعلية ضمن السلسلة النصية، يمنك الهروب من اعتبارها دلالة على استبدال المتغير ضمنها بقيمته من خلال استخدام زوج إضافي منها، على النحو: >>> spam = 42 >>> f'This prints the value in spam: {spam}' 'This prints the value in spam: 42' >>> f'This prints literal curly braces: {{spam}}' 'This prints literal curly braces: {spam}' وبما أن هذه الطريقة تتيح كتابة أسماء المتغيرات والتعابير البرمجية ضمن سطر السلسلة النصية الأساسية نفسه، فإن الشيفرة بالنتيجة ستكون بمقروئية أعلى مقارنةً بالطرق القديمة لتنسيق السلاسل النصية. وجود هذه الطرق المتعددة يخالف الحكمة القائلة "لابد من وجود طريقة واحدة واضحة لإنجاز الأمر، ومن المفضل وجود طريقة واحدة فقط" من مبادئ بايثون التوجيهية العشرون، إلا أن السلاسل النصية التنسيقية f-strings قد جاءت كتطوير للغة بايثون (من وجهة نظري)، وكما أن أحد المبادئ التوجيهية يشير إلى أن "الواقع العملي غير مثالي"، فإن كنت تستخدم الإصدار 3.6 من بايثون وما بعده فاستخدم دومًا السلاسل النصية التنسيقية. أما في حال استخدامك للإصدارات الأقدم، فأنصحك باستخدام التابع ()format أو الاعتماد على محدد التحويل s%. إنشاء نسخ ضحلة عن القوائم lists من الممكن إنشاء سلاسل نصية أو قوائم جديدة من تلك الحالية باستخدام صياغة التجزئة، ولرؤية كيفية عملها اكتب التعليمات التالية في الصدفة التفاعلية: >>> 'Hello, world!'[7:12] # Create a string from a larger string. 'world' >>> 'Hello, world!'[:5] # Create a string from a larger string. 'Hello' >>> ['cat', 'dog', 'rat', 'eel'][2:] # Create a list from a larger list. ['rat', 'eel'] نضع رمز النقطتين الرأسيتين : بين فهرسي عنصر البداية والنهاية للسلسلة الجديدة المراد إنشاؤها، ولدى إهمال وضع فهرس البداية قبل النقطتين الرأسيتين كما في المثال '[Hello, world!'[:5، فيُعيّن حينها افتراضيًا إلى القيمة 0، أما إذا أهملنا فهرس النهاية بعد النقطتين الرأسيتين كما في المثال [:cat', 'dog', 'rat', 'eel'][2']، فيُعيّن افتراضيًا إلى فهرس العنصر الأخير من السلسلة الأم. أما إذا أهملت تعيين كلا الفهرسين، فسيتم تعيين فهرس البداية إلى 0 (أي بداية القائمة أو السلسلة) وفهرس النهاية إلى نهاية القائمة، الأمر الذي يمثل طريقة فعالة لإنشاء نسخة عن السلسلة: >>> spam = ['cat', 'dog', 'rat', 'eel'] >>> eggs = spam[:] >>> eggs ['cat', 'dog', 'rat', 'eel'] >>> id(spam) == id(eggs) False ومن الجدير بالملاحظة في المثال السابق أن هويات القائمتين spam وeggs مختلفتين رغم تطابق قيمهما، إذ أن السطر البرمجي [:]eggs = spam ينشئ نسخة ضحلة عن القائمة spam، في حين أن التعليمة eggs=spam ستنسخ فعليًا مرجع القائمة spam وتسنده إلى القائمة eggs، إلا أن استخدام التعليمة [:] قد يبدو غريبًا بعض الشيء، واستخدام الدالة ()copy من وحدة النسخ copy module لإنشاء نسخة ضحلة من القائمة تعد الطريقة الأعلى مقروئية: >>> # Pythonic Example >>> import copy >>> spam = ['cat', 'dog', 'rat', 'eel'] >>> eggs = copy.copy(spam) >>> id(spam) == id(eggs) False فمن المفضل معرفتك لهذه الصياغة الغريبة لحالات مصادفتك لشيفرات بايثون قد استخدمتها، أما في شيفراتك الخاصة، فلا ننصحك باستخدامها. وتذكر أن كلًا من [:] و ()copy.copy تُنشآن نسخًا ضحلة. الخلاصة ما من لغة برمجة إلا ولديها الاصطلاحات والممارسات الأفضل الخاصة بها. وقد ركّز هذا المقال على الطرق العملية التي يستخدمها مبرمجي بايثون لكتابة شيفرات "بايثونية" ما يضمن الاستخدام الأمثل لميزات صياغة بايثون البرمجية. ولعل حجر الأساس وجوهر الشيفرات البايثونية هو "مبادئ بايثون التوجيهية العشرون"، وهي عبارة عن توجيهات عامة للكتابة بلغة بايثون. إلا أن هذه الحكم العشرون اختيارية وليست إلزامية لكتابة شيفرات بايثون، ومع ذلك من الجيد تذكرها دومًا. تثير المسافات البادئة ذات المعنى (والتي يجب عدم الخلط بينها وبين المسافات البيضاء) معظم كم استغراب واحتجاج مبرمجي بايثون المبتدئين، ورغم كون جميع لغات البرمجة تقريبًا تستخدم المسافات البادئة بغية جعل الشيفرات أسهل للقراءة، إلا أن بايثون تفرضها كبديل للأقواس المعقوصة المستخدمة في باقي لغات البرمجة. ورغم كون العديد من مبرمجي بايثون يستخدمون التركيب (()range(len للحلقات التكرارية، إلا أن الدالة ()enumerate توفّر منهجية أوضح للحصول على رقم الفهرس والقيمة الموافقة له لدى المرور على سلسلة ما. وكذلك الأمر بالنسبة للعبارة with الأوضح والأقل تسببًا بالأخطاء للتعامل مع الملفات مقارنةً باستدعاء التابعين ()open و()close يدويًا، إذ تضمن العبارة with استدعاء التابع ()close عند انتهاء التنفيذ وخروجه من الكتلة الخاصة بها. ولدى بايثون العديد من الطرق للتعامل مع السلاسل النصية ومعالجتها، ولعل الطريقة الأقدم هي استخدام محدد التحويل s% لتحديد المواضع المراد تضمينها ضمن السلسلة الرئيسية كجزء منها، أما الطريقة الأحدث والتي غدت موجودة اعتبارًا من الأصدار 3.6 فهي استخدام السلاسل النصية التنسيقية f-strings، وتُستخدم من خلال البادئة f قبل السلسلة النصية المراد صياغتها وبحصر الأجزاء المراد تضمينها في السلسلة ضمن أقواس معقوصة. أما الصيغة [:] المستخدمة لإنشاء نسخ ضحلة عن القوائم قد غدت قديمة نسبيَا وقد لا تعد طريقة بايثونية، إلا أنها قد غدت طريقة شائعة لإنشاء النسخ الضحلة بسرعة. ترجمة -وبتصرف- لجزء من الفصل السادس "كتابة شيفرات بايثون" من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigart. اقرأ أيضًا المقال السابق: خرافات حول ممارسات تجنب أخطاء محتملة في شيفرات لغة بايثون كيفية تنسيق النصوص في بايثون اكتشاف دلالات الأخطاء في شيفرات لغة بايثون
-
تعرفنا في المقال السابق على مفهوم روائح الشيفرات Code Smells في لغة بايثون، والتي تعني دلالات وقوع الأخطاء، فبعض الإشارات قد تدل على وجود أخطاء خفية أو محتملة أو على كون مقروئية الشيفرة ضعيفة. إلا أن بعض دلالات الأخطاء هي في الواقع ليست مؤشرات لوقوع أخطاء بالمطلق، فعالم البرمجة مليء بالنصائح السيئة أو التي عفا عليها الزمن والتي أصبحت خارج السياق الحالي أو تلك التي ما زالت موجودة رغم انتهاء صلاحية فائدتها، وألقي اللوم في هذا الصدد على مؤلفي الكتب التقنية ممن يقدمون آرائهم الشخصية على أنها أفضل الممارسات. لا بد وأنه قد تم تقديم بعضًا من الممارسات التالية لك على أنها من دلالات وقوع الأخطاء، رغم كون معظمها ممارسات جيدة، ما دفعنا لتسميتها خرافات دلالات الأخطاء، فهي تحذيرات حريٌ بك تجاهلها، لا بل يجب عليك تجاهلها، دعونا نطلع على البعض منها. خرافة أن الدالة يجب أن تتضمن تعليمة قيمة معادة return وحيدة وفي نهايتها أتت فكرة "مُدخل وحيد – مُخرج وحيد" من نصيحة مُساءة الفهم من أيام لغات البرمجة assembly و FORTRAN، إذ كانت هذه اللغات تسمح بإدخال إجراء فرعي (وهي بنية شبيهة بالدالة) عند أي نقطة من الشيفرة، حتى في وسطها، ما يجعل من الصعب تنقيح الأجزاء التي تم تنفيذها من هذا الإجراء الفرعي، إلا أن الدوال الحالية لا تعاني من هذه المشكلة، إذ يبدأ تنفيذ الدالة من بدايتها دومًا، ومع ذلك بقيت النصيحة قائمة ليكون مفادها "يجب أن تتضمن الدوال والتوابع تعليمة return واحدة والتي يجب أن تكون تحديدًا في نهاية الدالة أو التابع". وخلال محاولة تنفيذ هذه النصيحة (وجود تعليمة return واحدة ضمن الدالة أو التابع)، ستضطر غالبًا إلى استخدام سلسلة معقدة من الجمل الشرطية، والتي ستكون أكثر إرباكًا من وجود عدة تعليمات return ضمن بناء الدالة أو التابع. خرافة أن الدالة يجب أن تتضمن تعليمة try واحدة على الأكثر لعل النصيحة التي مفادها "أن الدوال والتوابع يجب أن تؤدي مهمة واحدة فقط" جيدة إجمالًا، طالما أنها لم تُفهم على أن التعامل مع الاستثناءات يجب أن يتم في دالة منفصلة. لنأخذ الدالة التالية التي تبين ما إذا كان الملف المراد حذفه غير موجود أصلًا كمثال: >>> import os >>> def deleteWithConfirmation(filename): ... try: ... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'): ... os.unlink(filename) ... except FileNotFoundError: ... print('That file already did not exist.') … إذ يجادل مؤيدو خرافة دلالة الخطأ آنفة الذكر بأنه طالما أن الدالة معنية بتأدية مهمة واحدة، وطالما أن التعامل مع الأخطاء هو مهمة واحدة، لذا يجب تقسيم الدالة السابقة إلى دالتين. كما يجادلون بأنه عند استخدام التعليمة try-except ضمن الدالة، فيجب أن تكون هي التعليمة الأولى ضمنها، حاصرةً كامل شيفرة الدالة لتبدو كما يلي: >>> import os >>> def handleErrorForDeleteWithConfirmation(filename): ... try: ... _deleteWithConfirmation(filename) ... except FileNotFoundError: ... print('That file already did not exist.') ... >>> def _deleteWithConfirmation(filename): ... if (input('Delete ' + filename + ', are you sure? Y/N') == 'Y'): ... os.unlink(filename) … وهي شيفرة معقدة بلا ضرورة، إذ تم تعيين الدالة ()_deleteWithConfirmation لتكون خاصة باستخدام البادئة _ للدلالة على عدم إمكانية استدعائها مباشرةً، وإنما بشكل غير مباشر من خلال استدعاء الدالة ()handleErrorForDeleteWithConfirmation، إذ أن اسم هذه الدالة الجديدة غير ملائم، فنحن نستدعيها بنية حذف ملف، لا بنية التعامل مع الأخطاء الناتجة أثناء حذف ملف. إذًا يجب أن تكون الدوال صغيرة وبسيطة، ولكن هذا لا يعني أنها يجب أن تكون محدودة لأداء "أمر واحد" (بالمعنى الذي تفهمه)، فمن الجيد أن تتضمن الدوال عدة تعليمات try-except ودون أن تحصر إحداها كامل شيفرة الدالة. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن خرافة أن الوسيط المنطقي خيار سيئ يطلق على الوسطاء من النوع قيم منطقية Boolean الممررة لدى استدعاء الدوال أو التوابع اسم وسطاء الأعلام أو الرايات flag arguments، والراية في البرمجة هي قيمة تشير إلى عملية إعداد أو ضبط وفق النظام الثنائي، كما في السمتين enabled الدالة على التمكين وdisabled الدالة على إلغاء التمكين، إذ غالبًا ما يعبّر عن الراية بقيمة منطقية، وبذلك يمكننا التعبير عن هذه الإعدادات بأنها إما معيّنة True أو ملغاة False. وقد أتى الاعتقاد المغلوط بأن استخدام وسطاء الرايات لاستدعاء الدوال هو إجراء سيئ، استنادًا على الادعاء القائل بأنه بناءً على قيمة الراية فإن الدالة ستؤدي مهمتين مختلفتين تمامًا، كما في المثال التالي: def someFunction(flagArgument): if flagArgument: # Run some code... else: # Run some completely different code… في الواقع، إن كانت دالتك تبدو بهذا الشكل، فمن الأفضل حتمًا إنشاء دالتين منفصلتين بدلًا من إنشاء وسيط مهمته تحديد أي نصف من شيفرة الدالة سيعمل، إلا أن معظم الدوال ذات وسطاء الرايات ليست بهذا الشكل، فمثلًا من الممكن تمرير قيمة منطقية للوسيط الافتراضي القابل للعكس للدالة ()sorted بغية تحديد طريقة ترتيب الفرز، ففي مثل هذه الحالة من غير المجدي تقسيم الدالة إلى دالتين الأولى باسم ()sorted والأخرى باسم ()reverseSorted، ناهيك عن عبء التكرار المطلوب. وبالنتيجة فإن فكرة أن وسطاء الرايات سيئة دومًا هي محض خرافة. خرافة أن المتغيرات العامة خيار سيئ تمثّل كل من الدوال والتوابع ما يشبه البرامج الصغيرة ضمن برنامجك الرئيسي، إذ أنها تتضمن شيفرات فيها متغيرات محلية تُنسى بمجرد إعادة الدالة لقيمتها، بما يشبه كيفية نسيان متغيرات البرامج وتحرير الذاكرة بمجرد انتهائها، إلا أن الدوال معزولة، فإما أن تعمل شيفراتها بالشكل الصحيح أو أن تواجه أخطاءً اعتمادًا على الوسطاء الممررة إليها لدى استدعائها. إلا أن الدوال والتوابع التي تستخدم المتغيرات العامة تفقد جزءًا من هذه الانعزالية المفيدة، ففي حين أن كل متغير عام تستخدمه في الدالة سيعمل كمُدخل جديد إليها بما يشبه فكرة الوسطاء، إلا أن زيادة عدد الوسطاء يعني زيادة التعقيدية، ما يعني بدوره احتمالية أعلى لوقوع الأخطاء، فعند ظهور خطأ في دالة ما ناتج عن قيمة خاطئة لمتغير عام، فإن هذه القيمة قد تكون مُعينة في أي جزء من البرنامج الرئيسي، وفي هذه الحالة لا يكفي تحليل الشيفرة الخاصة بالدالة أو السطر من البرنامج الذي يتم فيه استدعاؤها بحثًا عن السبب المحتمل لهذه القيمة الخاطئة، بل لا بد من مراجعة كامل شيفرة البرنامج، ولهذا السبب يجب أن تقلل من استخدامك للمتغيرات العامة. فعلى سبيل المثال، لنتمعن في الدالة ()calculateSlicesPerGuest (التي تعني حساب عدد قطع الحلوى لكل ضيف) من البرنامج partyPlanner.py (منسق الحفلة) المُفترض، إذ ضمّنا فيه أرقام الأسطر لتقديم فكرة عن حجم البرنامج الكبير: 1504. def calculateSlicesPerGuest(numberOfCakeSlices): 1505. global numberOfPartyGuests 1506. return numberOfCakeSlices / numberOfPartyGuests وبفرض أننا واجهنا لدى تشغيل البرنامج الاستثناء التالي: Traceback (most recent call last): File "partyPlanner.py", line 1898, in <module> print(calculateSlicesPerGuest(42)) File "partyPlanner.py", line 1506, in calculateSlicesPerGuest return numberOfCakeSlices / numberOfPartyGuests ZeroDivisionError: division by zero يعرض البرنامج خطأ القسمة على صفر، وهو ناتج عن سطر القيمة المعادة return numberOfCakeSlices / numberOfPartyGuests، وبالتالي لا بد من ان قيمة المتغير numberOfPartyGuests تساوي الصفر ما سبب ظهور هذا الخطأ، والسؤال: أين تم إسناد قيمة الصفر لهذا المتغير؟ فهو متغير عام، وبالتالي من الوارد أن يتم هذا الأمر في أي مكان ضمن آلاف الأسطر التي يتكون منها هذا البرنامج، لكننا نعلم من معلومات متتبع الأخطاء أن استدعاء الدالة ()calculateSlicesPerGuest قد تم في السطر رقم 1898 من البرنامج. بالذهاب إلى هذا السطر، يمكننا التحقق من قيمة الوسيط الممرر كقيمة للمتغير numberOfPartyGuests، إلا أن هذا لا يعني بالضرورة العثور على سبب الخطأ، فالمتغير numberOfPartyGuests كما ذكرنا سابقًا عام، وقد تكون القيمة الصفرية قد أُسندت إليه في أي سطر من البرنامج قبل استدعاء الدالة. ومن الجدير بالملاحظة أن الثوابت العامة لا تعتبر إجراء برمجي ضعيف، كون قيمها لا تتغير أثناء تنفيذ البرنامج، فهي لا تضيف على الشيفرة مزيدًا من التعقيدية كما تفعل المتغيرات العامة، فالثوابت غير مقصودة بالجملة "المتغيرات العامة خيار سيئ". مما لا شك فيه أن استخدام المتغيرات العامة يزيد من عبء التنقيح اللازم وصولًا إلى القيمة المسببة للاستثناء، وهذا ما يجعل فكرة الاستخدام الواسع للمتغيرات العامة غير محبذة، ولكن القول بأن كل المتغيرات العامة سيئة هي بالطبع خرافة، فالمتغيرات العامة قد تكون مفيدة في البرامج الأصغر أو لتتبع الإعدادات المطبقة على كامل البرنامج. وإجمالًا إن كنت قادرًا على تجنب استخدام متغير عام، فهي دلالة على وجوب عدم استخدامه، أما التعميم القائل "المتغيرات العامة سيئة" فهو رأي مسرف في التبسيط. خرافة أن التعليقات غير ضرورية في الواقع، وجود تعليقات سيئة هو أمرٌ أسوأ من حالة عدم وجود تعليقات بالمطلق، فالتعليق المُتضمن لمعلومات قديمة أو مشوشة سيزيد من عبء العمل على المبرمج بدلًا من مساعدته في تحقيق فهم أفضل، إلا أنّ هذه المشكلة المحتملة في التعليقات قد اتُخذت ذريعةً للتعميم بأن التعليقات ككل سيئة، ويجادل أصحاب هذا الرأي بأنه بدلًا من إضاعة الوقت بالتعليقات، يجب استبدالها بشيفرات عالية المقروئية (لا تحتاج لتعليقات أصلًا) لدرجة أنهم يرون بأن البرامج يجب ألا تحتوي على تعليقاتٍ بالمطلق. تُكتب التعليقات باللغة الإنجليزية أو باللغة التي يتقنها المبرمج أيًا كانت، سامحةً للمبرمجين بتبادل المعلومات بطريقة لا يمكن لأسماء المتغيرات والدوال والأصناف وحدها فعلها، إلا أن كتابة تعليق مختصر مفيد ليس بالأمر السهل، فالتعليقات كالشيفرات، تتطلب منك إعادة الكتابة والمراجعة والتكرار عدة مرات وصولًا لأفضل صيغة. وبما أننا نستطيع فهم الشيفرات التي كتبناها مباشرة بعد الفروغ من كتابتها، فإننا قد نشعر بلا جدوى كتابة التعليقات معتبرين إياها عمل إضافي لا طائل منه، ما يجعل المبرمجين بالنتيجة على استعداد لتقبل وجهة النظر القائلة بأن "التعليقات غير ضرورية". ولعل الحالة الأكثر شيوعًا هي وجود برامج بتعليقات قليلة أو بدون تعليقات أكثر من تلك المحتوية على تعليقات كثيرة جدًا أو تعليقات مُضللة. وإجمالًا فإن رفضك للتعليقات يشبه تمامًا اختيارك للسباحة عبر المحيط الأطلسي كون السفر بالطائرة عبره آمن "فقط" بنسبة 99.999991 بالمائة. الخاتمة تشير رائحة الشيفرة أو ما أسميناه دلالات الأخطاء على وجود طريقة أفضل لكتابة شيفراتك، وهي لا تعني بالضرورة وجوب إجراء تغييرات، لكنها ترشدك لإلقاء نظرة ثانية تأكيدية. ولكن ليست كل دلالات الأخطاء فعلية، فبعضها لا يتعدى كونه خرافات، كالنصائح البرمجية التي لم تعد صالحة أو تلك التي أثبتت أنها تعود على المبرمج بنتائج عكسية، كخرافة وجوب استخدام تعليمة return أو كتلة try-except واحدة ضمن بناء الدالة، مع عدم استخدام وسطاء الأعلام أو المتغيرات العامة بالمطلق، ناهيك عن الاعتقاد القائل بأن التعليقات غير ضرورية. تذكر دائمًا بأنه لا يوجد مقياس موضوعي لأفضل الممارسات، فمع اكتسابك للمزيد من الخبرة، ستخرج باستنتاجات عديدة حول ما يجعل شيفرتك أكثر مقروئية وموثوقية. ترجمة -وبتصرف- للجزء الثاني من الفصل الخامس Finding Code Smells من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigarti. اقرأ أيضًا المقال السابق: اكتشاف دلالات الأخطاء في شيفرات لغة بايثون ثلاثة أخطاء عليك تفاديها عند تعلم البرمجة بلغة بايثون التعامل مع رسائل الأخطاء في بايثون
-
مما لا شك فيه أن الشيفرة التي تسبب توقف عمل البرنامج هي خاطئة حُكمًا، والسؤال: هل هذا النوع من الأعطال هو الدليل الوحيد على وجود مشاكل في البرنامج؟ بالطبع لا، فبعض الإشارات قد تدل على وجود أخطاء خفية أو على كون مقروئية الشيفرة ضعيفة. وكما هو الحال مع رائحة الغاز التي قد تشير إلى وجود تسرّب في الغاز أو رائحة الدخان التي قد تشير لنشوب حريق، "فرائحة" الشيفرة Code Smells هي نمط للشيفرة المصدرية يشير لوجود أخطاء محتملة، ولا يعني بالضرورة وجود مشكلة، إلا أنه يدل على ضرورة التحقق من الشيفرة. سنقدم في هذا المقال عددًا من "روائح" الشيفرات الأكثر شيوعًا التي يحتمل أن تحمل أخطاءً، وانطلاقًا من مبدأ درهم وقاية خير من قنطار علاج، سيستغرق تجنب وقوع الأخطاء وقتًا وجهدًا أقل من مواجهتها وفهمها وإصلاحها لاحقًا، فلكل مبرمج مغامراته في قضاء ساعات بالتنقيح ليكتشف لاحقًا أن إصلاح الخطأ يشمل سطر برمجي واحد من الشيفرة، ولهذا السبب حتى أبسط دلالة على خطأ محتمل يجب أن تستوقفك، موجهةً إياك لإجراء المزيد من التحقق والتأكد من كونك لا تتسبب بحدوث مشاكل مستقبلية. وبالتأكيد لا تعني "رائحة" الشيفرة وجود مشكلة بالضرورة، وبالنتيجة وكما اعتدنا فالربان هو أنت، والقرار حول التعامل مع هذه الرائحة أو إهمالها هو قرار شخصي يعود إليك. قبل التعرف على كيفية اكتشاف أخطاء الشيفرات في لغة بايثون، ندعوك للتعرف على الأخطاء البرمجية عامةً أولًا والتعرف على كيفية التعامل معها: والآن دعونا نتحدث عن دلالات وجود الأخطاء في شيفرات لغة بايثون. تكرار الشيفرات إحدى أكثر دلالات الأخطاء (روائح الشيفرات) شيوعًا هي الشيفرات المكررة، وهي بالتعريف أي شيفرة مصدرية مُنشأة باستخدام نسخ ولصق أجزاءً من شيفرة أُخرى ضمن شيفرتك الحالية عدة مرات، فمثلًا يتضمن البرنامج التالي شيفراتٍ مكررة، إذ نلاحظ أنها تسأل المستخدم عن حاله (?How are you feeling) ثلاث مرات: print('Good morning!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') print('Good afternoon!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') print('Good evening!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') وتعد الشيفرات المكررة مشكلةً كونها تجعل من تعديل الشيفرة أصعب، فتعديل واحدة من النسخ المكررة يتطلب منك تعديل باقي النسخ جميعها، وفي حال نسيانك لإجراء التعديل في إحدى النسخ أو في حال إجراء تعديلات مختلفة عن بعضها للنسخ، فسينتهي الأمر يظهر أخطاء عند التنفيذ. والحل للشيفرات المكررة يكون بإلغاء تكرارها، بجعلها تظهر مرة واحدة ضمن الشيفرة وتكرارها بالعدد المطلوب باستخدام تابع ما أو حلقة، ففي المثال التالي تخلصنا من التكرار عبر حصر الجزء المكرر من الشيفرة ضمن تابع، لنستدعي التابع عدة مرات على التوالي لاحقًا: def askFeeling(): print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') print('Good morning!') askFeeling() print('Good afternoon!') askFeeling() print('Good evening!') askFeeling() أما في المثال التالي، فتخلصنا من التكرار عبر حصر الجزء المكرر ضمن حلقة: for timeOfDay in ['morning', 'afternoon', 'evening']: print('Good ' + timeOfDay + '!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') كما من الممكن دمج كلا تقنيتي التخلص من التكرار باستخدام دالة وحلقة معًا، بالشكل: def askFeeling(timeOfDay): print('Good ' + timeOfDay + '!') print('How are you feeling?') feeling = input() print('I am happy to hear that you are feeling ' + feeling + '.') for timeOfDay in ['morning', 'afternoon', 'evening']: askFeeling(timeOfDay) ونلاحظ أن الشيفرة المسؤولة عن عرض رسائل الترحيب "!Good morning/afternoon/evening" أي صباح/ظهر/مساء الخير الناتجة بعض التخلص من التكرار متشابهة ولكنها غير متطابقة، ففي التطوير الثالث لحل المشكلة ثبتنا الأجزاء المتطابقة من الشيفرة لتجنب التكرار، في حين يحل المعامل timeOfDay ومتغير الحلقة timeOfDay محل الأجزاء المتغيرة، وبذلك وبعد التخلص من التكرار عبر إزالة النسخ الزائدة، ما علينا سوى التعديل في مكان واحد متى ما احتجنا ذلك. وكما هو الحال مع كافة دلالات وقوع الأخطاء، فإن التخلص من التكرار ليست بالقاعدة الصارمة التي عليك اتباعها دومًا، فالمعيار هو طول القسم المكرر أو عدد مرات تكرار النسخ، فبزيادة أي منهما تصبح الحاجة إلى إلغاء التكرار أكثر إلحاحًا، فمثلًا لا نعد تكرار الشيفرات لمرة أو اثنتين دلالة لوقوع الأخطاء، وأنا شخصيًا أفكر بإزالة التكرار في حال وجود ثلاث أو أربع نسخ مكررة في برنامجي فأكثر. ففي بعض الأحيان قد تكون الشيفرة أبسط من أن تستحق عناء إلغاء التكرار، فلو قارنا الشيفرة الأولى من هذا القسم بالأخيرة، ورغم كون الشيفرة الأولى المكررة أطول، إلا أنها بسيطة ومباشرة، وتقوم الشيفرة الأخيرة بعد إزالة التكرار عمليًا بالوظيفة نفسها إلا أنها تتضمن حلقة ومتغير لها باسم timeOfDay، بالإضافة إلى تابع ذي معامل باسم timeOfDay أيضًا. يعد تكرار الشيفرات من دلالات وقوع الأخطاء لأنه يجعل من شيفرتك أصعب للتعديل، ففي حال وجود العديد من التكرارات في برنامجك، يكون الحل بحصر الجزء المكرر ضمن حلقة أو دالة ليظهر لمرة واحدة فقط. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن الأرقام السحرية ليس عجبًا إن قلنا أنّ البرمجة تتضمن استخدام الأرقام، إلا أن بعض الأرقام التي تظهر ضمن شيفرتك المصدرية قد تكون مصدر إرباك لمبرمج آخر يقرؤها (أو حتى لك أنت بعد مرور عدة أسابيع على كتابتك لها) وهي ما ندعوه بالأرقام السحرية magic numbers أي تلك الأرقام في الشيفرة التي قد تبدو عشوائية أو عديمة السياق والمعنى، فعلى سبيل المثال، لاحظ الرقم 604800 في السطر البرمجي التالي: expiration = time.time() + 604800 تعيد الدالة ()time.time رقمًا صحيحًا يمثل الوقت الحالي، ويمكننا فهم أن المتغير expiration والذي يعني وقت الانتهاء يمثل لحظة زمنية ما بعد مرور زمن قدره 604800 ثانية، إلا أن هذا الرقم هو مصدر الإرباك الأساسي، وأول ما سيتبادر إلى الذهن "ما مغزى تاريخ انتهاء الصلاحية هذا؟"، وفي الواقع، تعليق بسيط قد يحل المشكلة، بالشكل: expiration = time.time() + 604800 # Expire in one week. يمكن عد التعليق السابق حلًا جيدًا، إلا أن الأفضل هو استبدال الأرقام السحرية بثوابت، وتعرّف الثوابت بأنها متغيرات تُكتب بأحرف كبيرة دلالةً على أن قيمها يجب ألا تتغير عن القيمة البدائية المُسندة لها، وعادةً ما نصرح عن الثوابت في قسم التصريحات العامة، أي في بداية ملف الشيفرة، كما في الشكل: # Set up constants for different time amounts: SECONDS_PER_MINUTE = 60 SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR SECONDS_PER_WEEK = 7 * SECONDS_PER_DAY --snip-- expiration = time.time() + SECONDS_PER_WEEK # Expire in one week. إذ يجب استخدام ثوابت مختلفة للأرقام السحرية المستخدمة لأغراض مختلفة، حتى إن كانت قيمة الرقم نفسها، فعلى سبيل المثال، تضم أوراق اللعب 52 ورقة كما تضم السنة 52 أسبوعًا، ولكن في حال استخدامك لهذين المقدارين في شيفرتك، عليك التمييز بينهما كما في المثال: NUM_CARDS_IN_DECK = 52 NUM_WEEKS_IN_YEAR = 52 print('This deck contains', NUM_CARDS_IN_DECK, 'cards.') print('The 2-year contract lasts for', 2 * NUM_WEEKS_IN_YEAR, 'weeks.') ولدى تشغيل الشيفرة السابقة، سيبدو الخرج بالشكل: This deck contains 52 cards. The 2-year contract lasts for 104 weeks. إذ أن استخدام ثوابت منفصلة يساعدك على تغيير قيمة كل منها على حدى مستقبلًا، ومن الجدير بالملاحظة أن قيمة الثابت يجب ألا تتغير أثناء تنفيذ البرنامج، ولكن هذا لا يعني أن المبرمج غير قادر على تحديث أو تغيير قيمته ضمن الشيفرة المصدرية، فمثلًا في حال تضمن إصدار جديد من الشيفرة السابقة على ورقة لعب إضافية وهي المهرج (جوكر)، فعندها من الممكن تغيير قيمة الثابت الدال على عدد أوراق اللعب لتصبح 53 بالشكل: NUM_CARDS_IN_DECK = 53 NUM_WEEKS_IN_YEAR = 52 كما من الممكن إسقاط مصطلح الرقم السحري على بعض القيم غير الرقمية، فمن الممكن مثلًا استخدام قيم من سلاسل نصية كثوابت، فلنأخذ مثلًا الشيفرة التالية التي تطلب من المستخدم إدخال اتجاه من الاتجاهات الأربعة لتعيد رسالة تحذيرية في حال كون الاتجاه المدخل هو الشمال north، فإن الخطأ الطباعي في كتابة كلمة "شمال" على الصورة "nrth" ستؤدي لوقوع مشكلة ستمنع البرنامج من عرض رسالة التحذير المطلوبة: while True: print('Set solar panel direction:') direction = input().lower() if direction in ('north', 'south', 'east', 'west'): break print('Solar panel heading set to:', direction) 1 if direction == 'nrth': print('Warning: Facing north is inefficient for this panel.') والواقع أن هذا الخطأ من أنواع الأخطاء التي يصعب اكتشافها، نظرًا لكون السلسلة النصية nrth الواردة في السطر رقم 1 صحيحة من وجهة نظر بايثون رغم كونها خاطئة لغويًا، وبالتالي لن يتوقف البرنامج عن العمل، ولكن سنلاحظ عدم ظهور رسالة التحذير المتوقعة، أما في حال وقوعنا بنفس هذا الخطأ الطباعي مع استخدام الثوابت، فسيتوقف البرنامج عارضًا رسالة خطأ مفادها عدم وجود ثابت باسم NRTH كما يلي: # Set up constants for each cardinal direction: NORTH = 'north' SOUTH = 'south' EAST = 'east' WEST = 'west' while True: print('Set solar panel direction:') direction = input().lower() if direction in (NORTH, SOUTH, EAST, WEST): break print('Solar panel heading set to:', direction) 1 if direction == NRTH: print('Warning: Facing north is inefficient for this panel.') إن السطر رقم 1 من الشيفرة السابقة مع الخطأ الطباعي في كلمة NRTH سيثير لدى تشغيله استثناء NameError جاعلًا مصدر الخطأ جليًا على الفور: Set solar panel direction: west Solar panel heading set to: west Traceback (most recent call last): File "panelset.py", line 14, in <module> if direction == NRTH: NameError: name 'NRTH' is not defined إذًا تعد الأرقام السحرية من دلالات وقوع الأخطاء كونها لا تعبّر عن الغرض من استخدامها، جاعلةً من الشيفرة أقل مقروئية وأصعب للتطوير وعرضة لأخطاء إملائية يصعب اكتشافها، ويكون الحل باستخدام الثوابت بدلًا منها. إلغاء الشيفرات بتحويلها إلى تعليقات ومفهوم الشيفرة الميتة لعل تحويل الشيفرات إلى تعليقات بحيث لا تُنفّذ يمثّل إجراءً مؤقتًا جيدًا، لا سيما حين ترغب بتجاهل بعض الأسطر بغية اختبار آلية عمل باقي الأسطر وحدها، وبتحويل الأسطر المرغوب بتجاهلها يعد إجراء مناسب من حيث سهولة إعادتها إلى العمل جددًا لاحقًا، ولكن ماذا لو بقي السطر البرمجي كتعليق؟ سيكون مصدرًا للغموض في شيفرتك من حيث سبب إزالته أو تحت أي شروط قد نصبح بحاجة لوجوده مجدًدا. لنأخذ المثال التالي: doSomething() #doAnotherThing() doSomeImportantTask() doAnotherThing() تثير هذه الشيفرة العديد من التساؤلات، لم حوّلت الدالة ()doAnotherThing إلى تعليق؟ هل من المتوقع أن نحتاجها لاحقًا؟ لمَ لم يتم إقصاؤها لدى استدعائها للمرة الثانية؟ هل كانت هذه الشيفرة في الأصل تستدعي الدالة ()doAnotherThing مرتين؟ أم أنه استدعاء واحد أصلًا وتم نقله إلى ما بعد استدعاء الدالة ()doSomeImportantTask؟ هل من سبب واضح لعدم حذف هذه الدالة المستبعدة؟ في الواقع، ما من إجابات متاحة بسهولة لكل هذه التساؤلات. فالشيفرة الميتة Dead code بالتعريف هي كل شيفرة لا يمكن تنفيذها منطقيًا، كالشيفرات المتوضعة ضمن بناء الدالة ولكن بعد تعليمة الإعادة، أو الشيفرات المصورة ضمن جملة شرطية ذات شرط غير محقق على الدوام، أو الشيفرات ضمن دالة لا يتم استدعاؤها أبدًا، وكمثال عملي حول الشيفرات الميتة، اكتب المثال التالي ضمن الصدفة التفاعلية: >>> import random >>> def coinFlip(): ... if random.randint(0, 1): ... return 'Heads!' ... else: ... return 'Tails!' ... return 'The coin landed on its edge!' ... >>> print(coinFlip()) Tails! في الشيفرة السابقة، يمكن عد القيمة المعادة "!The coin landed on its edge" كشيفرة ميتة، إذ أن الشيفرة ستعيد قيم لحالات تحقق وعدم تحقق الشرط قبل أن يتمكن التنفيذ من بلوغ هذا السطر. قد تسبب الشيفرات الميتة الضياع لأن المبرمج القارئ لهذه الشيفرة سيعتبرها كجزء فعال من البرنامج في حين أن تأثيرها على الخرج لا يتعدى تأثير التعليقات عليه. يُستثنى من دلالات الأخطاء الشيفرة النائب Stubs، وهي عبارة عن مواضع مؤقتة للشيفرات المستقبلية، كالدوال والأصناف التي لم يتم تطبيقها بعد، فبدلًا من استخدام شيفرات حقيقية، يتضمن النائب عبارة مرور pass فقط، والتي لا تقوم بأي دور فعليًا (تسمى أيضًا العبارة عديمة الدور no operation أو no-op)، فالتعليمة pass موجودة لتستخدمها في الأماكن المفروض استخدام تعليمات فيها، في حين أنك لا ترغب بإضافتها الآن، لتنشئ بدلًا من ذلك شيفرة نائب مؤقتة، كما في المثال: >>> def exampleFunction(): ... pass … إن استدعاء الدالة السابقة لن يقوم بأي وظيفة، سوى الإشارة إلى أنه ستتم إضافة شيفرات له لاحقًا. وكبديل عن استدعاء تابع لا يقوم بأي دور، يمكنك جعله يعرض بالنيابة (عن دوره المتوقع المستقبلي) رسالة مفادها أن هذه الدالة ليست جاهزة للاستدعاء بعد باستخدام التعليمة raise NotImplementedError، بالشكل: >>> def exampleFunction(): ... raise NotImplementedError ... >>> exampleFunction() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in exampleFunction NotImplementedError ظهور الرسالة NotImplementedError سيمثل تنبيه لحالة استدعاء البرنامج لدالة أو تابع نائب عن طريق الصدفة. إذًا تعد كل من الشيفرات الملغاة بتحويلها إلى تعليقات والشيفرات الميتة من مؤشرات وقوع أخطاء، إذ أنها قد تضيع المبرمج بظنها جزء يجب تنفيذه من الشيفرة، واحذف بدلًا من ذلك هذه الشيفرات واستخدم نظام إدارة شيفرة مثل Git أو Subversion مما يتيح لك تتبع التغييرات دائمًا، فباستخدام هذه الأنظمة يمكنك حذف أي أجزاء تريد من الشيفرة وإعادتها لاحقًا بسهولة متى رغبت. التنقيح باستخدام دالة الطباعة التنقيح باستخدام دالة الطباعة هو الإجراء المتمثل باستدعاء الدالة ()print مؤقتًا ضمن البرنامج بغية عرض قيم المتغيرات قبل تشغيل البرنامج، وعادةً ما تمر هذه العملية بالخطوات التالية: ملاحظة وجود خطأ في البرنامج. إضافة بعض الاستدعاءات للدالة ()print لبعض المتغيرات في محاولة معرفة ما تحتويه. إعادة تشغيل البرنامج. إضافة المزيد من الاستدعاءات للدالة ()print لأن الاستدعاءات السابقة لم تعرض ما يكفي من معلومات. إعادة تشغيل البرنامج. تكرار الخطوتين السابقتين عدة مرات إلى حين اكتشاف موضع الخطأ. إعادة تشغيل البرنامج. إدراك أنك قد نسيت حذف بعضًا من استدعاءات الدالة ()print، فتحذف ما تبقى منها. إن التنقيح باستخدام دالة الطباعة مغرٍ ببساطته، إلا أنه يتطلب في الغالب إعادة تشغيل البرنامج مراتٍ ومرات قبل استعراض المعلومات التي تحتاجها فعلًا لإصلاح الخطأ، ويكون الحل البديل باستخدام منقح الأخطاء أو إنشاء مجموعة من الملفات السجل logfiles لبرنامجك، فباستخدام منقح الأخطاء يمكنك تنفيذ شيفرتك سطرًا تلو الآخر فاحصًا أي متغير تريد، وقد يبدو استخدام منقح الأخطاء هذا أبطأ من مجرد إضافة استدعاءات لدالة الطباعة ببساطة، إلا أنّه يوفّر عليك الوقت الضائع بالتشغيل المتكرر. في حين أن ملفات السجل قادرة على توفير كمية كبيرة من المعلومات حول برنامجك عبر مقارنة ملف سجل حالي بآخر سابق، وتوفر الوحدة logging في بايثون آلية سهلة لإنشاء ملفات السجل باستخدام شيفرة بسيطة مكونة من ثلاث سطور فقط: import logging logging.basicConfig(filename='log_filename.txt', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s') logging.debug('This is a log message.') فبعد استيراد وحدة السجل logging وضبط إعداداتها الرئيسية، يصبح من الممكن استدعاء التابع ()logging.debug لكتابة معلومات السجل ضمن مستند نصي، في حين أن الدالة ()print تعرض معلوماتها على شاشة الخرج. وعلى النقيض من التنقيح باستخدام دالة الطباعة، فإن استدعاء الدالة ()logging.debug يجعل من الواضح أي أجزاء من الخرج هي معلومات تنقيحية وأيها خرج البرنامج الفعلي. المتغيرات ذات اللاحقات الرقمية قد تحتاج أثناء كتابة البرامج إلى عدة متغيرات لتخزن النوع نفسه من البيانات، وفي هذه الحالات قد تميل لاستخدام الاسم نفسه مع إضافة لاحقة رقمية إلى الاسم، فعلى سبيل المثال في حال كنت تتعامل مع نموذج تسجيل دخول يطلب من مستخدميه إدخال كلمة مرورهم مرتين للتأكد من خلوها من الأخطاء، فقد تخزّن السلاسل النصية الممثلة لكلمتي المرور هاتين ضمن متغيرين بالأسماء password1 وpassword2. وإلى جانب أن هذه الأسماء لا تعبّر عن مضمون المتغيرات أو الاختلافات فيما بينها، فهي لا تشير أيضًا إلى عدد المتغيرات المشابهة الإجمالي، تاركةً القارئ في حيرة متسائلًا: أيوجد متغير أيضًا باسم password3 أو password4 ربما؟ فحاول دائمًا استخدام أسماء معبرة ومميزة للمتغيرات ولا تتكاسل بمجرد إضافة لاحقة رقمية لاسم المتغير السابق، ولعل أفضل تسمية للمتغيرين في مثالنا هذا هي password للأول وconfirm_password للثاني. لنأخذ مثالًا آخر، بفرض أنه لدينا تابع يتعامل مع إحداثيات بداية ونهاية، فقد تسمي المعاملات حينها x1 و y1 و x2 و y2 على التوالي، إلا أن هذه الأسماء لا تقدّم معلوماتٍ كما لو أسميتها start_x و start_y و end_x و end_y، ناهيك عن كون الاسمين start_x و start_y أكثر ترابطًا من x1 وy1 كتعبير عن تمثيلهما لإحداثيات نقطة البداية start. أما في حال تجاوز عدد المتغيرات ذات اللواحق الرقمية لاثنين، فعندها من المستحسن استخدام بنية معطيات كالقائمة list أو المجموعة set لتخزين هذه البيانات ضمن هيكلية، فمثلًا لو كان لدينا مجموعة من المتغيرات بالأسماء pet1Name و pet1Name وpet1Name وهكذا فمن الممكن تخزين قيمها ضمن قائمة واحدة باسم petNames. ومن الجدير بالملاحظة أن اللاحقة الرقمية لا تعد دلالة على الخطأ في أي متغير منتهٍ برقم، فمثلًا يعتبر الاسم enableIPV6 مثاليًا لمتغير، لأن الرقم 6 هو جزء من اسم البروتوكول "IPV6" ولا يمثّل لاحقة رقمية. أما في حال كنت ممن يستخدمون اللواحق الرقمية لتسمية سلسلة من المتغيرات، فمن المفضّل بدلًا من ذلك تخزين قيمها ضمن بنية معطيات ما، كقائمة أو قاموس أو غيرها. الأصناف التي يجب أن تكون مجرد دوال أو توابع اعتاد المبرمجون ممن يستخدمون لغات برمجة مثل جافا Java على إنشاء الأصناف بغية تنظيم شيفرات برامجهم، لنأخذ على سبيل المثال الصنف التالي المسمى Dice (بمعنى حجر النرد) والمتضمّن للتابع ()roll (والمقصود به رمي حجر النرد): >>> import random >>> class Dice: ... def __init__(self, sides=6): ... self.sides = sides ... def roll(self): ... return random.randint(1, self.sides) ... >>> d = Dice() >>> print('You rolled a', d.roll()) You rolled a 1 فقد تبدو الشيفرة السابقة بأنها شيفرة عالية التنظيم، ولكن ماذا لو فكرنا باحتياجاتنا الفعلية من هذه الشيفرة؟ أليست مجرد الحصول على رقم عشوائي محصور بين 1 و 6، وبالتالي من الممكن استبدال كامل الصنف السابق باستدعاء بسيط لتابع بالشكل: >>> print('You rolled a', random.randint(1, 6)) You rolled a 6 فبالمقارنة مع لغات البرمجة الأخرى، تستخدم لغة بايثون منهجية غير رسمية في تنظيم الشيفرات، لأن شيفراتها لا تتطلب تضمينها في صنف أو أي بنية متداولة أخرى، فإن كنت تُنشئ الكائنات بغية استدعاء تابع وحيد ضمنها، أو تُنشئ الأصناف لتحتوي فقط على توابع ساكنة غير مرتبطة بالكائنات، فكل هذه الإجراءات من دلالات الأخطاء التي تشير لأفضلية كتابة دوال اعتيادية بدلًا منها. إذ أننا نستخدم في بايثون الوحدات لتجميع الدوال مع بعضها البعض بدلًا من استخدام الأصناف، ذلك لأن الأصناف بحد ذاتها يجب أن تتواجد ضمن وحدات بطبيعة الحال، ما يجعل تضمين الدوال في أصناف مجرد إضافة غير ضرورية لمستوى تنظيمي جديد لشيفرتك، وتناقش الفصول من 15 حتى 17 في كتابنا هذا مبادئ التصميم كائني التوجّه تفصيليًا، كما تحدث Jack Diederich’s في خطابه ضمن مؤتمر بايثون لعام 2012 تحت عنوان Stop Writing Classes "كفوا عن كتابة الأصناف" حول العديد من الطرق التي يستخدمها المبرمجون، مضيفين بذلك تعقيدًا غير ضروريًا لشيفراتهم المكتوبة في بايثون. بنى اشتمالات القوائم المتداخلة يعد اشتمال القوائم List comprehensions طريقة مختصرة لإنشاء قائمة ذات قيم معقدة، فمثلًا لإنشاء قائمة سلاسل محرفية متضمنةً الأرقام من 0 حتى 100 باستثناء مضاعفات العدد 5، فعادةً ما ستستخدم حلقة for بالشكل: >>> spam = [] >>> for number in range(100): ... if number % 5 != 0: ... spam.append(str(number)) ... >>> spam ['1', '2', '3', '4', '6', '7', '8', '9', '11', '12', '13', '14', '16', '17', --snip-- '86', '87', '88', '89', '91', '92', '93', '94', '96', '97', '98', '99'] وبدلًا من ذلك، يمكنك الحصول على نفس النتيجة مُستخدمًا سطرًا برمجيًا واحدًا بالاعتماد على صيغة اشتمال القائمة: >>> spam = [str(number) for number in range(100) if number % 5 != 0] >>> spam ['1', '2', '3', '4', '6', '7', '8', '9', '11', '12', '13', '14', '16', '17', --snip-- '86', '87', '88', '89', '91', '92', '93', '94', '96', '97', '98', '99'] كما تمتلك لغة بايثون صيغًا لاشتمال كل من القواميس والمجموعات: 1 >>> spam = {str(number) for number in range(100) if number % 5 != 0} >>> spam {'39', '31', '96', '76', '91', '11', '71', '24', '2', '1', '22', '14', '62', --snip-- '4', '57', '49', '51', '9', '63', '78', '93', '6', '86', '92', '64', '37'} 2 >>> spam = {str(number): number for number in range(100) if number % 5 != 0} >>> spam {'1': 1, '2': 2, '3': 3, '4': 4, '6': 6, '7': 7, '8': 8, '9': 9, '11': 11, --snip-- '92': 92, '93': 93, '94': 94, '96': 96, '97': 97, '98': 98, '99': 99} أنشأنا في السطر ذو الرقم 1 بنية اشتمال مجموعة باستخدام الأقواس المعقوصة بدلًا من المعقوفة لتُنتج مجموعة من القيم، أما في السطر ذو الرقم 2 فأنشأنا بنية اشتمال قاموس باستخدام محرف النقطتين الرأسيتين لفصل المفتاح عن القيم ضمن الاشتمال. وبذلك نجد أن بنى الاشتمال مختصرة وقادرة على زيادة مقروئية شيفراتك، ومن الجدير بالملاحظة أن الاشتمالات تولّد قائمة أو مجموعة أو قاموس اعتمادًا على كائن تكراري (ففي مثالنا السابق حصلنا على القيمة المعادة من الكائن range من خلال الاستدعاء (range(100)، فالقوائم والمجموعات والقواميس بطبيعتها كائنات تكرارية، ما يعني إمكانية استخدام اشتمالات متداخلة كما في المثال التالي: >>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> nestedStrList = [[str(i) for i in sublist] for sublist in nestedIntList] >>> nestedStrList [['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']] إلا أن بنى اشتمالات القوائم المتداخلة (أو اشتمالات المجموعات أو القواميس المتداخلة) تنطوي على تعقيدية عالية ضمن جزء صغير من الشيفرة، جاعلةً من الشيفرة أصعب للقراءة، لذا من المفضّل توسيع اشتمال القائمة التالي واستبداله بحلقة for أو اثنتين: >>> nestedIntList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> nestedStrList = [] >>> for sublist in nestedIntList: ... nestedStrList.append([str(i) for i in sublist]) ... >>> nestedStrList [['0', '1', '2', '3'], ['4'], ['5', '6'], ['7', '8', '9']] كما من الممكن أن تتضمن الاشتمالات على عدة تعابير for، رغم أن هذا الإجراء قد ينطوي أيضًا على تقليل مقروئية الشيفرة، فمثلًا الاشتمال التالي يعطي بالنتيجة قائمةً واحدة انطلاقًا من مجموعة قوائم متداخلة: >>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> flatList = [num for sublist in nestedList for num in sublist] >>> flatList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] إذ يتضمن الاشتمال السابق تعبيري for، ما يجعله صعبًا للفهم حتى على أمهر مطوري بايثون، في حين أن توسعة الاشتمال السابق باستبداله بحلقتي for سيعطي نفس الناتج ولكن بطريقة أسهل وأوضح للقراءة والفهم: >>> nestedList = [[0, 1, 2, 3], [4], [5, 6], [7, 8, 9]] >>> flatList = [] >>> for sublist in nestedList: ... for num in sublist: ... flatList.append(num) ... >>> flatList [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] إذًا تعد الاشتمالات كاختصارات قادرة على اختصار الشيفرات شريطة عدم المبالغة والذهاب نحو خيار الاشتمالات المتداخلة صعبة القراءة والفهم. كتل الاستثناءات except الفارغة ورسائل الأخطاء الضعيفة لعل التقاط الاستثناءات واحدة من الطرق الرئيسية لضمان استمرار عمل برامجك حتى في حال ظهور المشاكل، فبمجرد ظهور استثناء دون وجود كتلة استثناء (القسم except فارغ) للتعامل معه سيتوقف برنامج بايثون عن العمل مباشرةً، ما قد يسبب خسارتك لعملك غير المحفوظ أو ترك الملفات دون اكتمالها. ومن الممكن تجنّب هذه الأعطال بتزويد شيفرتك بكتلة استثناء متضمنةً شيفرة للتعامل مع الخطأ، ولكن قد يكون من الصعب اتخاذ القرار الأنسب حول كيفية التعامل مع الخطأ، ما يجعل المبرمجين يميلون لترك كتلة الاستثناء فارغة باستخدام تعليمة pass ضمنها، ففي المثال التالي استخدمنا التعليمة pass لإنشاء كتلة استثناء لا تقوم فعليًا بأي عمل: >>> try: ... num = input('Enter a number: ') ... num = int(num) ... except ValueError: ... pass ... Enter a number: forty two >>> num 'forty two' لن تتوقف الشيفرة السابقة عن العمل في حال تمرير القيمة النصية 'forty two' إلى الدالة ()int، ذلك لأنه قد تم التعامل مع خطأ القيمة ValueError الذي تنتجه الدالة ()int في حالة تمرير نص بدلًا من رقم إليها من خلال عبارة الاستثناء، إلا أن عدم القيام بأي إجراء حيال الخطأ والاكتفاء بالهروب منه باستخدام استثناء فارغ قد يكون أسوأ من توقف البرنامج نتيجة هذا الخطأ، إذ أن البرامج بتوقفها تتفادى إكمال التنفيذ ببيانات خاطئة أو بنى غير مكتملة، التي قد تؤدي لأخطاء أكبر لاحقًا. على سبيل المثال، برنامجنا السابق لا يتوقف عن العمل حتى في حال تمرير محارف غير عددية إلى الدالة ()int، وبذلك يصبح المتغير num متضمنًا لسلسلة نصية بدلًا من رقم صحيح، ما قد يؤدي لوقوع أخطاء في أي مكان يُستخدم فيه هذا المتغير، فنستنتج أن عبارة الاستثناء هذه لا تقوم بما هو أكثر من إخفاء الخطأ بدلًا من مواجهته والتعامل معه. كما أن التعامل مع الاستثناءات برسائل أخطاء ضعيفة هو أيضًا من دلالات الوقوع في الأخطاء، كما في المثال التالي: >>> try: ... num = input('Enter a number: ') ... num = int(num) ... except ValueError: ... print('An incorrect value was passed to int()') ... Enter a number: forty two An incorrect value was passed to int() إن الشيفرة السابقة لن تتوقف عن العمل، وهو أمر جيد، إلا أنها بالمقابل لا تعطي المستخدم معلوماتٍ كافية لإرشاده حول كيفية إصلاح الخطأ، فيجب ألا ننسى أن رسائل الخطأ موجهة للمستخدمين وليست للمبرمجين، ففي رسالة الخطأ السابقة ناهيك كونها تتضمن معلومات تقنية لن يفهمها المستخدم العادي، كالإشارة إلى الدالة ()int، إلا أنها لا تخبر المُستخدم بما عليه فعله لإصلاح الخطأ، فالأصل أن تشرح رسالة الخطأ ما حدث مُرشدةً المستخدم لكيفية التعامل معه. قد يكون من الأسهل على المبرمج كتابة رسالة خطأ سريعة بتوصيف غير مفيد حيال ما حدث، بدلًا من كتابة الخطوات التفصيلية التي من شأنها مساعدة المستخدم على إصلاح المشكلة. ولكن تذكر دائمًا أنه لا يمكن اعتبار شيفرتك مكتملة ما لم تأخذ في الحسبان كل الاستثناءات الممكنة وكيفية التعامل معها. الخاتمة تشير رائحة الشيفرة أو ما أسميناه دلالات الأخطاء على وجود طريقة أفضل لكتابة شيفراتك، وهي لا تعني بالضرورة وجوب إجراء تغييرات، لكنها ترشدك لإلقاء نظرة ثانية تأكيدية. ولعل أشهر دلالات الأخطاء هي الشيفرات المكررة، والتي تشير إلى ضرورة تجنب التكرار عبر حصر الكود المكرر ضمن دالة أو حلقة، وبذلك نضمن بأن أي تغييرات مستقبلية ستجرى لمرة واحدة فقط في مكانٍ واحد. المصدر الثاني لدلالات الأخطاء هو الأرقام السحرية والتي تعرف بأنها أرقام تبدو عشوائية أو ليست ذات معنى ضمن الشيفرة والتي يمكن استبدالها باستخدام ثوابت بأسماء معبرة. كذلك الأمر بالنسبة للشيفرات الملغاة بجعلها تعليقات والشيفرات الميتة التي لن تُنفذ أبدًا من قبل الحاسوب، إلا أنها قد تشكل مصدر إرباك للمبرمج القارئ للشيفرة، فالأفضل حذف هذا النوع من الشيفرات والاعتماد على نظام إدارة شيفرات مثل Git في حال الرغبة بإعادة أي منها إلى الشيفرة مستقبلًا. أما تنقيح الأخطاء المعتمد على دالة الطباعة فيعني استدعاء الدالة ()print بغية عرض معلومات تنقيحية، ورغم سهولة هذه الطريقة، إلا أنه من الأسرع الاعتماد على منقح الأخطاء وملفات السجلات في تشخيص الأخطاء. ولعل الإجراء الأفضل للتعامل مع المتغيرات ذات اللواحق الرقمية مثل x1 و x2 و x3 وهكذا، هو تضمين قيمها في متغير واحد ضمن قائمة. وعلى خلاف لغات البرمجة الأخرى مثل لغة جافا، فإننا في لغة بايثون نستخدم الوحدات بدلًا من الأصناف في تجميع الدوال، فالصنف المتضمن لتابع وحيد أو لتوابع ساكنة هو أحد دلالات الأخطاء، إذ من المحبذ تضمين هذه الشيفرة في وحدة بدلًا من الصنف. أما بالنسبة لبنى اشتمال القوائم ورغم كونها طريقة مختصرة في إنشاء قوائم القيم، إلا أن تداخل هذه البنى يجعلها غير مقروءة. بالإضافة لكون أي استثناء يتم التعامل معه بكتلة استثناء فارغة هو من دلالات الأخطاء، حيث أنك ببساطة تخفي الخطأ بدلًا من التعامل معه، ولا تنسى ظان عدم وجود رسالة خطأ أفضل للمستخدم من رسالة قصيرة مبهمة. مما لا شك فيه أنك لن تطبيق كامل النصائح البرمجية الموصوفة كدلالات على وقوع الأخطاء الواردة في هذا المقال، وتذكر دائمًا بأنه لا يوجد مقياس موضوعي لأفضل الممارسات، فمع اكتسابك للمزيد من الخبرة، ستخرج باستنتاجات عديدة حول ما يجعل شيفرتك أكثر مقروئية وموثوقية، وهذا لا يلغي أن التوصيات الواردة في هذا المقال تغطي مسائل بالغة الأهمية يجب مراعاتها. ترجمة -وبتصرف- للجزء الأول من الفصل الخامس Finding Code Smells من كتاب Beyond the Basic Stuff with Python لصاحبه Al Sweigarti. اقرأ أيضًا المقال السابق: اختيار أسماء برمجية مفهومة في بايثون حل المشكلات وأهميتها في احتراف البرمجة