اذهب إلى المحتوى

غرائب بايثون المخفية


محمد الخضور

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

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

1st.png

الشكل 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)

فبالنتيجة يًقيّم التعبير في المثال السابق كما في المخطط التالي:

2nd.png

يمثّل التعبير البرمجي [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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...