عن بيثون
Python هي لغة برمجة عالية التفسير موجهة للكائنات ذات دلالات حيوية. تجعل هياكل البيانات عالية المستوى المدمجة مع الكتابة الديناميكية والربط الديناميكي جذابة للغاية لـ BRPS (التطوير السريع لأدوات التطبيق) ، فضلاً عن استخدامها كبرمجة نصية وتوصيل لغة للاتصال بالمكونات أو الخدمات الموجودة. يدعم Python الوحدات النمطية والحزم ، مما يشجع نمطية البرنامج وإعادة استخدام الكود.
حول هذا المقال
يمكن أن تكون البساطة وسهولة تعلم هذه اللغة مربكة للمطورين (خاصة أولئك الذين بدأوا لتعلم Python) ، لذلك يمكنك أن تغفل بعض التفاصيل الدقيقة وتقلل من قوة مجموعة متنوعة من الحلول الممكنة باستخدام Python.
مع وضع هذا في الاعتبار ، تقدم هذه المقالة "أفضل 10" من الأخطاء الدقيقة التي يصعب العثور عليها والتي يمكن أن يقوم بها مطورو Python المتقدمون.
الخطأ رقم 1: إساءة استخدام التعبيرات كقيم افتراضية للوسائط الوظيفية
يسمح لك Python بالإشارة إلى أن الوظيفة يمكن أن تحتوي على وسيطات اختيارية من خلال تعيين قيمة افتراضية لها. هذا ، بطبيعة الحال ، هو سمة مريحة للغاية للغة ، ولكن يمكن أن يؤدي إلى عواقب غير سارة إذا كان نوع هذه القيمة قابلاً للتغيير. على سبيل المثال ، ضع في الاعتبار تعريف الوظيفة التالي:
>>> def foo(bar=[]):
يتمثل الخطأ الشائع في هذه الحالة في الاعتقاد بأن قيمة الوسيطة الاختيارية سيتم تعيينها على القيمة الافتراضية في كل مرة يتم استدعاء دالة دون قيمة لهذه الوسيطة. في التعليمة البرمجية أعلاه ، على سبيل المثال ، يمكننا أن نفترض أنه عن طريق استدعاء الدالة foo () مرارًا وتكرارًا (أي ، دون تحديد قيمة للوسيطة bar) ، ستظهر دائمًا "baz" ، حيث يُفترض أنه في كل مرة يتم استدعاء foo () (بدون تحديد شريط الوسيطة) ، يتم تعيين الشريط إلى [] (أي ، قائمة فارغة جديدة).
ولكن دعونا نرى ما سيحدث بالفعل:
>>> foo() ["baz"] >>> foo() ["baz", "baz"] >>> foo() ["baz", "baz", "baz"]
و؟ لماذا تستمر الوظيفة في إضافة القيمة الافتراضية "baz" إلى القائمة الموجودة في كل مرة يتم استدعاء foo () ، بدلاً من إنشاء قائمة جديدة في كل مرة؟
سيكون الجواب على هذا السؤال هو فهم أعمق لما يجري مع بيثون "تحت الغطاء". وهي: القيمة الافتراضية للوظيفة تتم تهيئتها مرة واحدة فقط ، خلال تعريف الوظيفة. وبالتالي ، تتم تهيئة وسيطة الشريط افتراضيًا (أي ، قائمة فارغة) فقط عندما يتم تعريف foo () لأول مرة ، ولكن المكالمات اللاحقة إلى foo () (على سبيل المثال ، دون تحديد وسيطة الشريط) ستستمر في استخدام نفس القائمة التي كانت تم إنشاؤه لشريط الوسيطة في وقت التعريف الأول للدالة.
كمرجع ، "الحل البديل" الشائع لهذا الخطأ هو التعريف التالي:
>>> def foo(bar=None): ... if bar is None:
الخطأ رقم 2: إساءة استخدام متغيرات الفئة
النظر في المثال التالي:
>>> class A(object): ... x = 1 ... >>> class B(A): ... pass ... >>> class C(A): ... pass ... >>> print Ax, Bx, Cx 1 1 1
يبدو أن كل شيء في محله.
>>> Bx = 2 >>> print Ax, Bx, Cx 1 2 1
نعم ، كان كل شيء كما هو متوقع.
>>> Ax = 3 >>> print Ax, Bx, Cx 3 2 3
ماذا بحق الجحيم؟ لقد غيرنا Ax. لماذا تغير Cx أيضًا؟
في بيثون ، يتم التعامل مع المتغيرات في الفصل مثل القواميس وتتبع ما يسمى غالبًا بأمر دقة الأسلوب (MRO). وبالتالي ، في التعليمة البرمجية أعلاه ، نظرًا لعدم العثور على السمة x في الفئة C ، سيتم العثور عليها في الفئات الأساسية (A فقط في المثال أعلاه ، على الرغم من أن Python تدعم الوراثة المتعددة). بمعنى آخر ، لا تمتلك C خاصية خاصة بها x مستقلة عن A. وبالتالي ، فإن الإشارات إلى Cx هي في الواقع إشارات إلى Ax. سيؤدي ذلك إلى حدوث مشاكل إذا لم يتم التعامل مع هذه الحالات بشكل صحيح. لذلك عندما تتعلم بايثون ، انتبه بشكل خاص للسمات الصفية والعمل معها.
الخطأ رقم 3: معلمات غير صحيحة لكتلة الاستثناء
افترض أن لديك الكود التالي:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except ValueError, IndexError:
المشكلة هنا هي أن تعبير الاستثناء لا يقبل قائمة الاستثناءات المحددة بهذه الطريقة. بدلاً من ذلك ، في Python 2.x ، يُستخدم تعبير "باستثناء Exception، e" لربط الاستثناء بمعلمة ثانية ثانية معيّنة (في هذه الحالة ، e) لإتاحتها لمزيد من الفحص. نتيجة لذلك ، في التعليمة البرمجية أعلاه ، لا يتم اكتشاف استثناء IndexError بواسطة العبارة باستثناء ؛ بدلاً من ذلك ، ينتهي الاستثناء بربط معلمة باسم IndexError بدلاً من ذلك.
تتمثل الطريقة الصحيحة لالتقاط استثناءات متعددة باستخدام تعبير الاستثناء في تحديد المعلمة الأولى كلعبة تحتوي على جميع الاستثناءات التي تريد التقاطها. أيضًا ، لتحقيق أقصى توافق ، استخدم الكلمة الأساسية ، حيث أن بناء الجملة هذا مدعوم في كل من Python 2 و Python 3:
>>> try: ... l = ["a", "b"] ... int(l[2]) ... except (ValueError, IndexError) as e: ... pass ... >>>
الخطأ رقم 4: سوء فهم قواعد نطاق بايثون
يستند النطاق في Python إلى ما يسمى بقانون LEGB ، وهو اختصار لـ Local (الأسماء التي تم تعيينها بأي شكل من الأشكال داخل دالة (def أو lambda) ، ولم يتم الإعلان عنها عالميًا في هذه الوظيفة) ، Enclosure (الاسم في النطاق المحلي لأي وظائف تتضمن بشكل ثابت) def أو lambda) ، من داخلي إلى خارجي) ، Global (الأسماء المعينة في المستوى الأعلى من ملف الوحدة النمطية ، أو عن طريق تنفيذ الإرشادات العامة الواردة في def داخل الملف) ، مدمج (أسماء تم تعيينها مسبقًا في وحدة اسم مدمجة: open ، range ، SyntaxError ، ...). يبدو بسيطا بما فيه الكفاية ، أليس كذلك؟ حسنًا ، في الواقع ، هناك بعض التفاصيل الدقيقة لكيفية عمل هذا في Python ، وهو ما يقودنا إلى مشكلة برمجة Python العامة الأكثر تعقيدًا أدناه. النظر في المثال التالي:
>>> x = 10 >>> def foo(): ... x += 1 ... print x ... >>> foo() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in foo UnboundLocalError: local variable 'x' referenced before assignment
ما هي المشكلة؟
يحدث الخطأ أعلاه لأنه عند تعيين متغير في النطاق ، يعتبر Python تلقائيًا أنه محلي لهذا النطاق ويخفي أي متغير يحمل نفس الاسم في أي نطاق أصل.
وبالتالي ، يفاجأ الكثيرون عند تلقيهم UnboundLocalError في التعليمة البرمجية التي يتم تشغيلها مسبقًا ، عندما يتم تعديلها عن طريق إضافة عامل تعيين في مكان ما في نص الدالة.
هذه الميزة مربكة خاصة للمطورين عند استخدام القوائم. النظر في المثال التالي:
>>> lst = [1, 2, 3] >>> def foo1(): ... lst.append(5)
و؟ لماذا يتعطل foo2 بينما يعمل foo1 بشكل جيد؟
الجواب هو نفسه كما في المثال السابق ، ولكن وفقًا للاعتقاد السائد ، فإن الموقف هنا أكثر دقة. لا تطبق foo1 عامل التخصيص على lst ، بينما لا تطبق foo2. مع الأخذ في الاعتبار أن lst + = [5] هي في الحقيقة مجرد اختصار لـ lst = lst + [5] ، نرى أننا نحاول تعيين القيمة lst (لذلك يفترض Python أن يكون في النطاق المحلي). ومع ذلك ، تستند القيمة التي نريد تعيينها إلى lst إلى lst نفسها (مرة أخرى ، يُفترض الآن أنها في النطاق المحلي) ، والتي لم يتم تحديدها بعد. ونحن حصلنا على خطأ.
الخطأ رقم 5: تغيير قائمة أثناء التكرار
يجب أن تكون المشكلة في جزء التعليمات البرمجية التالي واضحة إلى حد ما:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> for i in range(len(numbers)): ... if odd(numbers[i]): ... del numbers[i]
تعتبر إزالة عنصر من قائمة أو صفيف أثناء التكرار فوقه مشكلة بيثون معروفة لأي مطور برامج ذي خبرة. ولكن ، على الرغم من أن المثال أعلاه قد يكون واضحًا إلى حد ما ، يمكن للمطورين ذوي الخبرة الشروع في هذا أشعل النار في رمز أكثر تعقيدًا.
لحسن الحظ ، تتضمن Python عددًا من نماذج البرمجة الأنيقة التي ، عند استخدامها بشكل صحيح ، يمكن أن تؤدي إلى تبسيط رمز كبير وتحسينه. من النتائج السارة الإضافية لذلك أنه في رمز أبسط ، يكون احتمال الوقوع في الخطأ بحذف عنصر قائمة بطريق الخطأ أثناء التكرار فوقه أقل بكثير. أحد هذه النماذج هو قائمة المولدات. بالإضافة إلى ذلك ، فهم تشغيل مولدات القوائم مفيد بشكل خاص في تجنب هذه المشكلة بالذات ، كما هو موضح في هذا التطبيق البديل للرمز أعلاه ، والذي يعمل بشكل جيد:
>>> odd = lambda x : bool(x % 2) >>> numbers = [n for n in range(10)] >>> numbers[:] = [n for n in numbers if not odd(n)]
الخطأ رقم 6: سوء فهم كيف تربط بيثون المتغيرات في عمليات الإغلاق
النظر في المثال التالي:
>>> def create_multipliers(): ... return [lambda x : i * x for i in range(5)] >>> for multiplier in create_multipliers(): ... print multiplier(2) ...
يمكنك توقع الإخراج التالي:
0 2 4 6 8
ولكن في الواقع تحصل على هذا:
8 8 8 8 8
مفاجأة!
هذا بسبب الارتباط المتأخر في Python ، مما يعني أن قيم المتغيرات المستخدمة في عمليات الإغلاق يتم البحث عنها أثناء استدعاء الوظيفة الداخلية. وبالتالي ، في التعليمة البرمجية أعلاه ، عندما يتم استدعاء أي من الوظائف التي يتم إرجاعها ، يتم البحث في القيمة i في النطاق المحيط أثناء المكالمة (وبحلول ذلك الوقت كانت الدورة قد اكتملت بالفعل ، لذلك تم بالفعل تعيين النتيجة النهائية - القيمة 4) .
إن الحل لمشكلة بايثون المشتركة هذه هو:
>>> def create_multipliers(): ... return [lambda x, i=i : i * x for i in range(5)] ... >>> for multiplier in create_multipliers(): ... print multiplier(2) ... 0 2 4 6 8
فويلا! نستخدم الوسائط الافتراضية هنا لإنشاء وظائف مجهولة لتحقيق السلوك المطلوب. قد يسميه البعض هذا الحل الأنيق. البعض هم
رقيقة. بعض الناس يكرهون هذه الأشياء. لكن إذا كنت من مطوري بيثون ، فمن المهم أن تفهم ذلك.
خطأ # 7: إنشاء تبعيات وحدة دوري
افترض أن لديك ملفين ، a.py و b.py ، يستورد كل منهما الآخر ، كما يلي:
في a.py:
import b def f(): return bx print f()
في b.py:
import a x = 1 def g(): print af()
أولاً ، حاول استيراد a.py:
>>> import a 1
انها عملت على ما يرام. هذا قد يفاجئك. بعد كل شيء ، وحدات استيراد بعضها البعض بشكل دوري وهذا ربما ينبغي أن يكون مشكلة ، أليس كذلك؟
الجواب هو أن مجرد وجود استيراد دوري للوحدات النمطية ليس بحد ذاته مشكلة في بيثون. إذا كانت الوحدة قد تم استيرادها بالفعل ، فإن بايثون ذكي بما يكفي لعدم محاولة إعادة استيراده. ومع ذلك ، بناءً على النقطة التي تحاول كل وحدة من خلالها الوصول إلى الوظائف أو المتغيرات المحددة في وحدة أخرى ، قد تواجه بالفعل مشكلات.
لذا ، بالعودة إلى مثالنا ، عندما قمنا باستيراد a.py ، لم تواجه أي مشاكل في استيراد b.py ، لأن b.py لا يتطلب تعريف أي من a.py أثناء الاستيراد. المرجع الوحيد في b.py إلى دعوة إلى af (). ولكن هذه الدعوة في g () ولا شيء في a.py أو b.py لا تستدعي g (). لذلك كل شيء يعمل بشكل جيد.
ولكن ماذا يحدث إذا حاولنا استيراد b.py (دون استيراد a.py أولاً ، أي):
>>> import b Traceback (most recent call last): File "<stdin>", line 1, in <module> File "b.py", line 1, in <module> import a File "a.py", line 6, in <module> print f() File "a.py", line 4, in f return bx AttributeError: 'module' object has no attribute 'x'
اوه هذا ليس جيد! المشكلة هنا هي أنه أثناء عملية استيراد b.py ، يحاول استيراد a.py ، والذي بدوره يستدعي f () ، والذي يحاول الوصول إلى bx لكن bx لم يتم تعريفه بعد. ومن هنا استثناء AttributeError.
حل واحد على الأقل لهذه المشكلة هو تافهة جدا. فقط قم بتعديل b.py لاستيراد a.py إلى g ():
x = 1 def g(): import a
الآن عندما نستوردها ، كل شيء على ما يرام:
>>> import b >>> bg() 1
الخطأ رقم 8: تقاطع الأسماء مع أسماء الوحدات النمطية في مكتبة Python القياسية
واحدة من سحر بيثون هو العديد من الوحدات التي تخرج من الصندوق. لكن كنتيجة لذلك ، إذا لم تتبع ذلك بوعي ، فقد تجد أن اسم الوحدة النمطية قد يكون بنفس اسم الوحدة النمطية في المكتبة القياسية التي تأتي مع Python (على سبيل المثال ، قد يكون هناك وحدة نمطية بالاسم email.py ، والذي سيتعارض مع وحدة المكتبة القياسية التي تحمل الاسم نفسه).
هذا يمكن أن يؤدي إلى مشاكل خطيرة. على سبيل المثال ، إذا حاولت أي من الوحدات النمطية استيراد إصدار الوحدة النمطية من مكتبة Python القياسية ، وكان لديك وحدة نمطية في المشروع بنفس الاسم ، والتي سيتم استيرادها عن طريق الخطأ بدلاً من الوحدة النمطية من المكتبة القياسية.
لذلك ، يجب توخي الحذر حتى لا تستخدم نفس الأسماء الموجودة في الوحدات النمطية لمكتبة Python القياسية. يعد تغيير اسم الوحدة النمطية في مشروعك أسهل بكثير من تقديم طلب لتغيير اسم الوحدة النمطية في المكتبة القياسية والحصول على الموافقة عليها.
الخطأ رقم 9: عدم مراعاة الاختلافات بين بيثون 2 وبيثون 3
النظر في ملف foo.py التالي:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def bad(): e = None try: bar(int(sys.argv[1])) except KeyError as e: print('key error') except ValueError as e: print('value error') print(e) bad()
في بيثون 2 ، ستعمل بشكل جيد:
$ python foo.py 1 key error 1 $ python foo.py 2 value error 2
ولكن الآن دعونا نرى كيف ستعمل في بيثون 3:
$ python3 foo.py 1 key error Traceback (most recent call last): File "foo.py", line 19, in <module> bad() File "foo.py", line 17, in bad print(e) UnboundLocalError: local variable 'e' referenced before assignment
ما حدث للتو هنا؟ "المشكلة" هي أنه في Python 3 ، لا يتوفر كائن في كتلة استثناء خارجه. (والسبب في ذلك هو أنه سيتم تخزين الكائنات الموجودة في هذه الكتلة في الذاكرة إلى أن يبدأ جامع البيانات المهملة ويزيل المراجع إليها من هناك).
إحدى الطرق لتجنب هذه المشكلة هي الاحتفاظ بالإشارة إلى كائن كتلة الاستثناء خارج هذه الكتلة بحيث تظل متوفرة. في ما يلي إصدار المثال السابق الذي يستخدم هذه التقنية ، وبالتالي الحصول على رمز مناسب لكل من Python 2 و Python 3:
import sys def bar(i): if i == 1: raise KeyError(1) if i == 2: raise ValueError(2) def good(): exception = None try: bar(int(sys.argv[1])) except KeyError as e: exception = e print('key error') except ValueError as e: exception = e print('value error') print(exception) good()
قم بتشغيله في Python 3:
$ python3 foo.py 1 key error 1 $ python3 foo.py 2 value error 2
URAAAA!
الخطأ رقم 10: الاستخدام غير السليم لطريقة __del__
لنفترض أن لديك ملف mod.py مثل هذا:
import foo class Bar(object): ... def __del__(self): foo.cleanup(self.myhandle)
وأنت تحاول القيام بذلك من another_mod.py آخر:
import mod mybar = mod.Bar()
والحصول على AttributeError الرهيبة.
لماذا؟ لأنه ، كما هو مذكور
هنا ، عندما يتم إيقاف تشغيل المترجم الفوري ، فإن جميع المتغيرات العامة للوحدة النمطية لها القيمة بلا. نتيجة لذلك ، في المثال أعلاه ، عندما تم استدعاء __del__ ، تم بالفعل تعيين الاسم foo على بلا.
الحل لهذه "المهمة مع علامة النجمة" هو استخدام atexit.register (). وبالتالي ، عندما يكمل برنامجك التنفيذ (أي عندما يخرج بشكل طبيعي) ، يتم حذف مقابضك قبل أن يكمل المترجم عمله.
مع وضع ذلك في الاعتبار ، قد يبدو إصلاح الكود mod.py أعلاه كالتالي:
import foo import atexit def cleanup(handle): foo.cleanup(handle) class Bar(object): def __init__(self): ... atexit.register(cleanup, self.myhandle)
يوفر مثل هذا التنفيذ طريقة بسيطة وموثوقة لاستدعاء أي تنظيف ضروري بعد إنهاء البرنامج العادي. من الواضح أن القرار الخاص بكيفية التعامل مع الكائن المرتبط باسم self.myhandle متروك لـ foo.cleanup ، لكنني أعتقد أنك تفهم الفكرة.
استنتاج
Python هي لغة قوية ومرنة مع العديد من الآليات والنماذج التي يمكن أن تحسن الأداء بشكل ملحوظ. ومع ذلك ، كما هو الحال مع أي أداة أو لغة برنامج ، مع فهم محدود أو تقييم قدراتها ، قد تنشأ مشاكل غير متوقعة أثناء التطوير.
تساعد مقدمة الفروق الدقيقة في بيثون التي تتناولها هذه المقالة في تحسين استخدامك للغة ، مع تجنب بعض الأخطاء الشائعة.