تُعد مجموعات القواعد التي تُعرّف لغات البرمجة معقدةً، وقد تؤدي إلى شيفرات ذات سلوكيات أو نتائج غريبة وغير متوقعة رغم كونها غير خاطئة. سنتعمق في هذا المقال أكثر بغرائب بايثون الغامضة. من غير المحتمل أن تواجه هذه الحالات لدى كتابة الشيفرات الواقعية، إلا أنها تمثّل استخدامات مثيرة للاهتمام لصياغة بايثون (أو مثل إساءات لاستخدام هذه الصياغة، حسب منظورك للأمر).
ستتعرف من خلال دراستك للأمثلة الواردة في هذا المقال على كيفية عمل بايثون في الكواليس بصورة أوضح، فهيا بنا لنستمتع باستكشاف بعضٍ من الصيغ الصحيحة النادرة والمؤدية إلى وقوع أخطاء 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: توفّر بايثون الذاكرة باستخدام مراجع متعددة لنفس كائن العدد الصحيح (على اليسار)، بدلًا من كائنات أعداد صحيحة مكررة منفصلة من أجل كل مرجع (على اليمين).
وبسبب التحسين آنف الذكر، فمن الممكن أن تؤدي بعض الحالات المفتعلة إلى نتائج غريبة، وكمثال على ذلك، لنكتب ما يلي في الشيفرة التفاعلية:
>>> 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.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.